From f58b0051558ae2f98688c46a2ff18b94d0fa929d Mon Sep 17 00:00:00 2001 From: David Vogel Date: Mon, 18 Jul 2022 22:07:53 +0200 Subject: [PATCH] Several fixes and improvements - Change vscode Lua runtime to LuaJIT - Remove worm entity overrides - Remove preparePlayer() that sets HP and other stuff - Change how entites are captured - Increase entity capture radius - Capture entities as soon as possible - Modify entites immediately after they are captured - Make stringArgs in print local - Fix JSON marshaling of Noita vectors - Fix NoitaEntity:GetComponents() and NoitaEntity:GetFirstComponent() - Add varArg support to NoitaComponent:SetValue() and NoitaComponent:ObjectSetValue() - Update some EmmyLua annotations - Fix custom GamePrint - Add table.pack function that is missing in LuaJIT --- .vscode/settings.json | 3 +- data/entities/animals/worm.xml | 229 ------------------- data/entities/animals/worm_big.xml | 299 ------------------------ data/entities/animals/worm_end.xml | 317 ------------------------- data/entities/animals/worm_skull.xml | 330 --------------------------- data/entities/animals/worm_tiny.xml | 248 -------------------- files/capture.lua | 167 +++++++++----- files/compatibility.lua | 8 +- files/noita-api.lua | 56 +++-- files/util.lua | 14 +- 10 files changed, 163 insertions(+), 1508 deletions(-) delete mode 100644 data/entities/animals/worm.xml delete mode 100644 data/entities/animals/worm_big.xml delete mode 100644 data/entities/animals/worm_end.xml delete mode 100644 data/entities/animals/worm_skull.xml delete mode 100644 data/entities/animals/worm_tiny.xml diff --git a/.vscode/settings.json b/.vscode/settings.json index 1241f06..c41611d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,6 @@ "xmin", "ymax", "ymin" - ] + ], + "Lua.runtime.version": "LuaJIT" } \ No newline at end of file diff --git a/data/entities/animals/worm.xml b/data/entities/animals/worm.xml deleted file mode 100644 index b1445c9..0000000 --- a/data/entities/animals/worm.xml +++ /dev/null @@ -1,229 +0,0 @@ - - <_Transform - position.x="0" - position.y="0" - rotation="0" - scale.x="1" - scale.y="1" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/entities/animals/worm_big.xml b/data/entities/animals/worm_big.xml deleted file mode 100644 index f0d286e..0000000 --- a/data/entities/animals/worm_big.xml +++ /dev/null @@ -1,299 +0,0 @@ - - <_Transform - position.x="0" - position.y="0" - rotation="0" - scale.x="1" - scale.y="1" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/entities/animals/worm_end.xml b/data/entities/animals/worm_end.xml deleted file mode 100644 index fab0227..0000000 --- a/data/entities/animals/worm_end.xml +++ /dev/null @@ -1,317 +0,0 @@ - - <_Transform - position.x="0" - position.y="0" - rotation="0" - scale.x="1" - scale.y="1" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/entities/animals/worm_skull.xml b/data/entities/animals/worm_skull.xml deleted file mode 100644 index ec8c18c..0000000 --- a/data/entities/animals/worm_skull.xml +++ /dev/null @@ -1,330 +0,0 @@ - - <_Transform - position.x="0" - position.y="0" - rotation="0" - scale.x="1" - scale.y="1" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/entities/animals/worm_tiny.xml b/data/entities/animals/worm_tiny.xml deleted file mode 100644 index c7994f9..0000000 --- a/data/entities/animals/worm_tiny.xml +++ /dev/null @@ -1,248 +0,0 @@ - - <_Transform - position.x="0" - position.y="0" - rotation="0" - scale.x="1" - scale.y="1" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/files/capture.lua b/files/capture.lua index d07cc94..a9e5c44 100644 --- a/files/capture.lua +++ b/files/capture.lua @@ -11,7 +11,6 @@ local json = dofile_once("mods/noita-mapcap/files/json-serialize.lua") CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio. CAPTURE_GRID_SIZE = 512 -- in virtual (world) pixels. There will always be exactly 4 images overlapping if the virtual resolution is 1024x1024. -CAPTURE_FORCE_HP = 4 -- * 25HP -- "Base layout" (Base layout. Every part outside this is based on a similar layout, but uses different materials/seeds) CAPTURE_AREA_BASE_LAYOUT = { @@ -37,16 +36,108 @@ CAPTURE_AREA_EXTENDED = { Bottom = 41984 -- in virtual (world) pixels. (Coordinate is not included in the rectangle) } -local function preparePlayer() - local playerEntity = getPlayer() - addEffectToEntity(playerEntity, "PROTECTION_ALL") +local componentTypeNamesToDisable = { + "AnimalAIComponent", + "SimplePhysicsComponent", + "CharacterPlatformingComponent", + "WormComponent", + "WormAIComponent", + "DamageModelComponent", + "PhysicsBodyCollisionDamageComponent", + "ExplodeOnDamageComponent", + "SpriteOffsetAnimatorComponent", + --"PhysicsBody2Component", -- Disabling will hide barrels and similar stuff, also triggers an assertion. + --"PhysicsBodyComponent", + --"VelocityComponent", -- Disabling this component may cause a "...\component_updators\advancedfishai_system.cpp at line 107" exception. + --"SpriteComponent", + --"AudioComponent", +} - --addPerkToPlayer("BREATH_UNDERWATER") - --addPerkToPlayer("INVISIBILITY") - --addPerkToPlayer("REMOVE_FOG_OF_WAR") - --addPerkToPlayer("REPELLING_CAPE") - --addPerkToPlayer("WORM_DETRACTOR") - setPlayerHP(CAPTURE_FORCE_HP) +--- +---@return file*|nil +local function createOrOpenEntityCaptureFile() + -- Make sure the file exists. + local file = io.open("mods/noita-mapcap/output/entities.json", "a") + if file ~= nil then file:close() end + + -- Create or reopen entities CSV file. + file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0. + if file == nil then return nil end + + return file +end + +---captureEntities gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and modifies those entities. +---@param entityFile file*|nil +---@param x number +---@param y number +---@param radius number +local function captureEntities(entityFile, x, y, radius) + if not entityFile then return end + + local entities = noitaAPI.Entity.GetInRadius(x, y, radius) + for _, entity in ipairs(entities) do + -- Get to the root entity, as we are exporting entire entity trees. + local rootEntity = entity:GetRootEntity() + -- Make sure to only export entities when they are encountered the first time. + if not rootEntity:HasTag("MapCaptured") then + + -- Some hacky way to generate valid JSON that doesn't break when the game crashes. + -- Well, as long as it does not crash between write and flush. + if entityFile:seek("end") == 0 then + -- First line. + entityFile:write("[\n\t", json.Marshal(rootEntity), "\n", "]") + else + -- Following lines. + entityFile:seek("end", -2) -- Seek a few bytes back, so we can overwrite some stuff. + entityFile:write(",\n\t", json.Marshal(rootEntity), "\n", "]") + end + + -- Prevent recapturing. + rootEntity:AddTag("MapCaptured") + + -- Disable some components. + for _, componentTypeName in ipairs(componentTypeNamesToDisable) do + local components = rootEntity:GetComponents(componentTypeName) + for _, component in ipairs(components) do + rootEntity:SetComponentsEnabled(component, false) + end + end + + -- Modify the gravity of every VelocityComponent, so stuff will not fall. + local component = rootEntity:GetFirstComponent("VelocityComponent") + if component then + component:SetValue("gravity_x", 0) + component:SetValue("gravity_y", 0) + end + + -- Modify the gravity of every CharacterPlatformingComponent, so mobs will not fall. + local component = rootEntity:GetFirstComponent("CharacterPlatformingComponent") + if component then + component:SetValue("pixel_gravity", 0) + end + + -- Disable the hover and spinning animations of every ItemComponent. + local component = rootEntity:GetFirstComponent("ItemComponent") + if component then + component:SetValue("play_hover_animation", false) + component:SetValue("play_spinning_animation", false) + end + + -- Disable the hover animation of cards. Disabling the "SpriteOffsetAnimatorComponent" does not help. + local components = rootEntity:GetComponents("SpriteOffsetAnimatorComponent") + for _, component in ipairs(components) do + component:SetValue("x_speed", 0) + component:SetValue("y_speed", 0) + component:SetValue("x_amount", 0) + component:SetValue("y_amount", 0) + end + + end + end + + -- Ensure everything is written to disk before noita decides to crash. + entityFile:flush() end --- Captures a screenshot at the given coordinates. @@ -81,60 +172,24 @@ local function captureScreenshot(x, y, rx, ry, entityFile) DrawUI() wait(0) UiCaptureDelay = UiCaptureDelay + 1 - until DoesWorldExistAt(xMin, yMin, xMax, yMax) -- Chunks will be drawn on the *next* frame. + + -- Capture all entities right after the camera frame was moved. + local ok, err = pcall(captureEntities, entityFile, x, y, 5000) + if not ok then + print(string.format("Entity capture error: %s", err)) + end + + until DoesWorldExistAt(xMin, yMin, xMax, yMax) and UiCaptureDelay > 25 -- Chunks will be drawn on the *next* frame. wait(0) -- Without this line empty chunks may still appear, also it's needed for the UI to disappear. if not TriggerCapture(rx, ry) then UiCaptureProblem = "Screen capture failed. Please restart Noita." end - -- Capture entities right after capturing the screenshot. - if entityFile then - local ok, err = pcall(function() - local radius = math.sqrt(virtualHalfWidth^2 + virtualHalfHeight^2) + 1 - local entities = noitaAPI.Entity.GetInRadius(x, y, radius) - for _, entity in ipairs(entities) do - -- Get to the root entity, as we are exporting entire entity trees. - local rootEntity = entity:GetRootEntity() - -- Make sure to only export entities when they are encountered the first time. - if not rootEntity:HasTag("MapCaptured") then - -- Some hacky way to generate valid JSON that doesn't break when the game crashes. - -- Well, as long as it does not crash between write and flush. - if entityFile:seek("end") == 0 then - -- First line. - entityFile:write("[\n\t", json.Marshal(rootEntity), "\n", "]") - else - -- Following lines. - entityFile:seek("end", -2) -- Seek a few bytes back, so we can overwrite some stuff. - entityFile:write(",\n\t", json.Marshal(rootEntity), "\n", "]") - end - - rootEntity:AddTag("MapCaptured") -- Prevent recapturing. - end - end - end) - if not ok then - print("Entity export error:", err) - end - entityFile:flush() -- Ensure everything is written to disk before noita decides to crash. - end - -- Reset monitor and PC standby each screenshot. ResetStandbyTimer() end -local function createOrOpenEntityCaptureFile() - -- Make sure the file exists. - local file = io.open("mods/noita-mapcap/output/entities.json", "a") - if file ~= nil then file:close() end - - -- Create or reopen entities CSV file. - file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0. - if file == nil then return nil end - - return file -end - function startCapturingSpiral() local entityFile = createOrOpenEntityCaptureFile() @@ -149,8 +204,6 @@ function startCapturingSpiral() local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2) - preparePlayer() - GameSetCameraFree(true) -- Coroutine to calculate next coordinate, and trigger screenshots. @@ -233,8 +286,6 @@ function startCapturingHilbert(area) UiProgress = {Progress = 0, Max = gridWidth * gridHeight} - preparePlayer() - GameSetCameraFree(true) -- Coroutine to calculate next coordinate, and trigger screenshots. diff --git a/files/compatibility.lua b/files/compatibility.lua index e01e025..d6dd006 100644 --- a/files/compatibility.lua +++ b/files/compatibility.lua @@ -1,4 +1,4 @@ --- Copyright (c) 2019-2020 David Vogel +-- Copyright (c) 2019-2022 David Vogel -- -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT @@ -9,8 +9,7 @@ local oldPrint = print function print(...) local arg = {...} - - stringArgs = {} + local stringArgs = {} for i, v in ipairs(arg) do table.insert(stringArgs, tostring(v)) @@ -23,8 +22,7 @@ end --[[local logFile = io.open("lualog.txt", "w") function print(...) local arg = {...} - - stringArgs = {} + local stringArgs = {} local result = "" for i, v in ipairs(arg) do diff --git a/files/noita-api.lua b/files/noita-api.lua index 328bfb1..32ae5a8 100644 --- a/files/noita-api.lua +++ b/files/noita-api.lua @@ -48,7 +48,15 @@ function NoitaComponent:MarshalJSON() if membersTable then for k, v in pairs(membersTable) do if not componentValueKeysWithInvalidType[k] then - members[k] = self:GetValue(k) -- Try to get value with correct type. Assuming nil is an error, but this is not always the case... meh. + local packedResult = table.pack(self:GetValue(k)) -- Try to get value with correct type. Assuming nil is an error, but this is not always the case... meh. + if packedResult.n == 0 then + members[k] = nil -- Write no result as nil. Basically do nothing. + elseif packedResult.n == 1 then + members[k] = packedResult[1] -- Write single value result as single value. + else + packedResult.n = nil -- Discard n field, otherwise this is not a pure array. + members[k] = packedResult -- Write multi value result as array. + end end if members[k] == nil then componentValueKeysWithInvalidType[k] = true @@ -187,10 +195,15 @@ end ---Returns a table of components filtered by the given parameters. ---@param componentTypeName string ----@param tag string +---@param tag string|nil ---@return NoitaComponent[] function NoitaEntity:GetComponents(componentTypeName, tag) - local componentIDs = EntityGetComponent(self.ID, componentTypeName, tag) or {} + local componentIDs + if tag ~= nil then + componentIDs = EntityGetComponent(self.ID, componentTypeName, tag) or {} + else + componentIDs = EntityGetComponent(self.ID, componentTypeName) or {} + end local result = {} for _, componentID in ipairs(componentIDs) do table.insert(result, setmetatable({ ID = componentID }, NoitaComponent)) @@ -200,10 +213,15 @@ end ---Returns the first component of this entity that fits the given parameters. ---@param componentTypeName string ----@param tag string +---@param tag string|nil ---@return NoitaComponent|nil function NoitaEntity:GetFirstComponent(componentTypeName, tag) - local componentID = EntityGetFirstComponent(self.ID, componentTypeName, tag) + local componentID + if tag ~= nil then + componentID = EntityGetFirstComponent(self.ID, componentTypeName, tag) + else + componentID = EntityGetFirstComponent(self.ID, componentTypeName) + end if componentID == nil then return nil end @@ -426,15 +444,15 @@ end ---@param fieldName string ---@return any|nil function NoitaComponent:GetValue(fieldName) - return ComponentGetValue2(self.ID, fieldName) + return ComponentGetValue2(self.ID, fieldName) -- TODO: Rework Noita API to handle vectors, and return a vector instead of some shitty multi value result end ---Sets the value of a field. Value(s) should have a type matching the field type. ---Reports error if the values weren't given in correct type, the field type is not supported, or the component does not exist. ---@param fieldName string ----@param valueOrValues any|nil -function NoitaComponent:SetValue(fieldName, valueOrValues) - return ComponentSetValue2(self.ID, fieldName, valueOrValues) +---@param ... any|nil -- Vectors use one argument per dimension. +function NoitaComponent:SetValue(fieldName, ...) + return ComponentSetValue2(self.ID, fieldName, ...) -- TODO: Rework Noita API to handle vectors, and use a vector instead of shitty multi value arguments end ---Returns one or many values matching the type or subtypes of the requested field in a component subobject. @@ -445,16 +463,16 @@ end ---@param fieldName string ---@return any|nil function NoitaComponent:ObjectGetValue(objectName, fieldName) - return ComponentObjectGetValue2(self.ID, objectName, fieldName) + return ComponentObjectGetValue2(self.ID, objectName, fieldName) -- TODO: Rework Noita API to handle vectors, and return a vector instead of some shitty multi value result end ---Sets the value of a field in a component subobject. Value(s) should have a type matching the field type. ---Reports error if the values weren't given in correct type, the field type is not supported or 'object_name' is not a metaobject. ---@param objectName string ---@param fieldName string ----@param valueOrValues any -function NoitaComponent:ObjectSetValue(objectName, fieldName, valueOrValues) - return ComponentObjectSetValue2(self.ID, objectName, fieldName, valueOrValues) +---@param ... any|nil -- Vectors use one argument per dimension. +function NoitaComponent:ObjectSetValue(objectName, fieldName, ...) + return ComponentObjectSetValue2(self.ID, objectName, fieldName, ...) -- TODO: Rework Noita API to handle vectors, and use a vector instead of shitty multi value arguments end ---Creates a component of type 'component_type_name' and adds it to 'entity_id'. @@ -473,26 +491,26 @@ function NoitaEntity:EntityAddComponent(componentTypeName, tableOfComponentValue return setmetatable({ ID = componentID }, NoitaComponent) end ----'type_stored_in_vector' should be "int", "float" or "string". +--- ---@param arrayMemberName string ----@param typeStoredInVector string +---@param typeStoredInVector "int"|"float"|"string" ---@return number function NoitaComponent:GetVectorSize(arrayMemberName, typeStoredInVector) return ComponentGetVectorSize(self.ID, arrayMemberName, typeStoredInVector) end ----'type_stored_in_vector' should be "int", "float" or "string". +--- ---@param arrayName string ----@param typeStoredInVector string +---@param typeStoredInVector "int"|"float"|"string" ---@param index number ---@return number|number|string|nil function NoitaComponent:GetVectorValue(arrayName, typeStoredInVector, index) return ComponentGetVectorValue(self.ID, arrayName, typeStoredInVector, index) end ----'type_stored_in_vector' should be "int", "float" or "string". +--- ---@param arrayName string ----@param typeStoredInVector string +---@param typeStoredInVector "int"|"float"|"string" ---@return number[]|number|string|nil function NoitaComponent:GetVector(arrayName, typeStoredInVector) return ComponentGetVector(self.ID, arrayName, typeStoredInVector) diff --git a/files/util.lua b/files/util.lua index 939c7b9..709ee95 100644 --- a/files/util.lua +++ b/files/util.lua @@ -1,4 +1,4 @@ --- Copyright (c) 2019-2020 David Vogel +-- Copyright (c) 2019-2022 David Vogel -- -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT @@ -23,7 +23,7 @@ function GamePrint(...) end for line in result:gmatch("[^\r\n]+") do - for i, v in ipairs(splitStringByLength(line, 100)) do + for i, v in ipairs(SplitStringByLength(line, 100)) do oldGamePrint(v) end end @@ -105,3 +105,13 @@ function progressBarString(progress, look) return string.format(look.Format, barString, progress.Progress, progress.Max, factor * 100) end + +---Returns a new table with all arguments stored into keys `1`, `2`, etc. and with a field `"n"` with the total number of arguments. +---@param ... any +---@return table +function table.pack(...) + t = {...} + t.n = select("#", ...) + + return t +end