-- Copyright (c) 2022 David Vogel -- -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT -- Noita modding API, but a bit more beautiful. -- Current modding API version: 7 -- State: Working but incomplete. If something is missing, add it by hand! -- It would be optimal to generate this API wrapper automatically... ---@type Vec2 local Vec2 = dofile_once("mods/noita-mapcap/files/libraries/vec2.lua") ---@type JSONLib local json = dofile_once("mods/noita-mapcap/files/libraries/json.lua") ------------- -- Classes -- ------------- local EntityAPI = {} ---@class NoitaEntity ---@field ID integer -- Noita entity ID. local NoitaEntity = {} NoitaEntity.__index = NoitaEntity ---WrapID wraps the given entity ID and returns a Noita entity object. ---@param id number ---@return NoitaEntity|nil function EntityAPI.WrapID(id) if id == nil or type(id) ~= "number" then return nil end return setmetatable({ ID = id }, NoitaEntity) end local ComponentAPI = {} ---@class NoitaComponent ---@field ID integer -- Noita component ID. local NoitaComponent = {} NoitaComponent.__index = NoitaComponent ---WrapID wraps the given component ID and returns a Noita component object. ---@param id number ---@return NoitaComponent|nil function ComponentAPI.WrapID(id) if id == nil or type(id) ~= "number" then return nil end return setmetatable({ ID = id }, NoitaComponent) end local CameraAPI = {} ------------------------- -- JSON Implementation -- ------------------------- -- Set of component keys that would return an "invalid type" error when called with ComponentGetValue2(). -- This is more or less to get around console error spam that otherwise can't be prevented when iterating over component members. -- Only used inside the JSON marshaler, until there is a better solution. local componentValueKeysWithInvalidType = {} ---MarshalJSON implements the JSON marshaler interface. ---@return string function NoitaComponent:MarshalJSON() -- Get list of members, but with correct type (instead of string values). local membersTable = self:GetMembers() local members = {} if membersTable then for k, v in pairs(membersTable) do if not componentValueKeysWithInvalidType[k] then 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 --members[k] = v -- Fall back to string value of self:GetMembers(). end end end local resultObject = { typeName = self:GetTypeName(), members = members, --objectMembers = component:ObjectGetMembers } return json.Marshal(resultObject) end ---MarshalJSON implements the JSON marshaler interface. ---@return string function NoitaEntity:MarshalJSON() local result = { name = self:GetName(), filename = self:GetFilename(), tags = self:GetTags(), children = self:GetAllChildren(), components = self:GetAllComponents(), transform = {}, } result.transform.x, result.transform.y, result.transform.rotation, result.transform.scaleX, result.transform.scaleY = self:GetTransform() return json.Marshal(result) end ------------------------ -- Noita API wrappers -- ------------------------ --- ---@param filename string ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- Y coordinate in world (virtual) pixels. ---@return NoitaEntity|nil function EntityAPI.Load(filename, posX, posY) local entityID = EntityLoad(filename, posX, posY) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end --- ---@param filename string ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- Y coordinate in world (virtual) pixels. ---@return NoitaEntity|nil function EntityAPI.LoadEndGameItem(filename, posX, posY) local entityID = EntityLoadEndGameItem(filename, posX, posY) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end --- ---@param filename string ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- Y coordinate in world (virtual) pixels. function EntityAPI.LoadCameraBound(filename, posX, posY) return EntityLoadCameraBound(filename, posX, posY) end ---Creates a new entity from the given XML file, and attaches it to entity. ---This will not load tags and other stuff, it seems. ---@param filename string ---@param entity NoitaEntity function EntityAPI.LoadToEntity(filename, entity) return EntityLoadToEntity(filename, entity.ID) end --- ---Note: works only in dev builds. ---@param filename string function NoitaEntity:Save(filename) return EntitySave(self.ID, filename) end --- ---@param name string ---@return NoitaEntity|nil function EntityAPI.CreateNew(name) local entityID = EntityCreateNew(name) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end --- function NoitaEntity:Kill() return EntityKill(self.ID) end --- function NoitaEntity:IsAlive() return EntityGetIsAlive(self.ID) end --- ---@param componentTypeName string ---@param tableOfComponentValues string[]|nil ---@return NoitaComponent|nil function NoitaEntity:AddComponent(componentTypeName, tableOfComponentValues) local componentID = EntityAddComponent(self.ID, componentTypeName, tableOfComponentValues) if componentID == nil then return nil end return setmetatable({ ID = componentID }, NoitaComponent) end --- ---@param component NoitaComponent function NoitaEntity:RemoveComponent(component) return EntityRemoveComponent(self.ID, component.ID) end ---Returns a table of with all components of this entity. ---@return NoitaComponent[] function NoitaEntity:GetAllComponents() local componentIDs = EntityGetAllComponents(self.ID) or {} local result = {} for _, componentID in ipairs(componentIDs) do table.insert(result, setmetatable({ ID = componentID }, NoitaComponent)) end return result end ---Returns a table of components filtered by the given parameters. ---@param componentTypeName string ---@param tag string|nil ---@return NoitaComponent[] function NoitaEntity:GetComponents(componentTypeName, tag) 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)) end return result end ---Returns the first component of this entity that fits the given parameters. ---@param componentTypeName string ---@param tag string|nil ---@return NoitaComponent|nil function NoitaEntity:GetFirstComponent(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 return setmetatable({ ID = componentID }, NoitaComponent) end ---Sets the transform of the entity. ---@param x number ---@param y number ---@param rotation number ---@param scaleX number ---@param scaleY number function NoitaEntity:SetTransform(x, y, rotation, scaleX, scaleY) return EntitySetTransform(self.ID, x, y, rotation, scaleX, scaleY) end ---Sets the transform and tries to immediately refresh components that calculate values based on an entity's transform. ---@param x number ---@param y number ---@param rotation number ---@param scaleX number ---@param scaleY number function NoitaEntity:SetAndApplyTransform(x, y, rotation, scaleX, scaleY) return EntityApplyTransform(self.ID, x, y, rotation, scaleX, scaleY) end ---Returns the transformation of the entity. ---@return number x, number y, number rotation, number scaleX, number scaleY function NoitaEntity:GetTransform() return EntityGetTransform(self.ID) end --- ---@param child NoitaEntity function NoitaEntity:AddChild(child) return EntityAddChild(self.ID, child.ID) end --- ---@return NoitaEntity[] function NoitaEntity:GetAllChildren() local entityIDs = EntityGetAllChildren(self.ID) or {} local result = {} for _, entityID in ipairs(entityIDs) do table.insert(result, setmetatable({ ID = entityID }, NoitaEntity)) end return result end --- ---@return NoitaEntity|nil function NoitaEntity:GetParent() local entityID = EntityGetParent(self.ID) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end ---Returns the given entity if it has no parent, otherwise walks up the parent hierarchy to the topmost parent and returns it. ---@return NoitaEntity function NoitaEntity:GetRootEntity() local entityID = EntityGetRootEntity(self.ID) return setmetatable({ ID = entityID }, NoitaEntity) end --- function NoitaEntity:RemoveFromParent() return EntityRemoveFromParent(self.ID) end --- ---@param tag string ---@param enabled boolean function NoitaEntity:SetComponentsWithTagEnabled(tag, enabled) return EntitySetComponentsWithTagEnabled(self.ID, tag, enabled) end --- ---@param component NoitaComponent ---@param enabled boolean function NoitaEntity:SetComponentsEnabled(component, enabled) return EntitySetComponentIsEnabled(self.ID, component.ID, enabled) end --- ---@return string function NoitaEntity:GetName() return EntityGetName(self.ID) end --- ---@param name string function NoitaEntity:SetName(name) return EntitySetName(self.ID, name) end ---Returns an array of all the entity's tags. ---@return string[] function NoitaEntity:GetTags() ---@type string local tagsString = EntityGetTags(self.ID) local result = {} for tag in tagsString:gmatch('([^,]+)') do table.insert(result, tag) end return result end ---Returns all entities with 'tag'. ---@param tag string ---@return NoitaEntity[] function EntityAPI.GetWithTag(tag) local entityIDs = EntityGetWithTag(tag) or {} local result = {} for _, entityID in ipairs(entityIDs) do table.insert(result, setmetatable({ ID = entityID }, NoitaEntity)) end return result end ---Returns all entities in 'radius' distance from 'x','y'. ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- X coordinate in world (virtual) pixels. ---@param radius number -- Radius in world (virtual) pixels. ---@return NoitaEntity[] function EntityAPI.GetInRadius(posX, posY, radius) local entityIDs = EntityGetInRadius(posX, posY, radius) or {} local result = {} for _, entityID in ipairs(entityIDs) do table.insert(result, setmetatable({ ID = entityID }, NoitaEntity)) end return result end ---Returns all entities in 'radius' distance from 'x','y' that have the given tag. ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- X coordinate in world (virtual) pixels. ---@param radius number -- Radius in world (virtual) pixels. ---@param tag string ---@return NoitaEntity[] function EntityAPI.GetInRadiusWithTag(posX, posY, radius, tag) local entityIDs = EntityGetInRadiusWithTag(posX, posY, radius, tag) or {} local result = {} for _, entityID in ipairs(entityIDs) do table.insert(result, setmetatable({ ID = entityID }, NoitaEntity)) end return result end --- ---@param posX number -- X coordinate in world (virtual) pixels. ---@param posY number -- X coordinate in world (virtual) pixels. ---@return NoitaEntity|nil function EntityAPI.GetClosest(posX, posY) local entityID = EntityGetClosest(posX, posY) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end --- ---@param name string ---@return NoitaEntity|nil function EntityAPI.GetWithName(name) local entityID = EntityGetWithName(name) if entityID == nil then return nil end return setmetatable({ ID = entityID }, NoitaEntity) end --- ---@param tag string function NoitaEntity:AddTag(tag) return EntityAddTag(self.ID, tag) end --- ---@param tag string function NoitaEntity:RemoveTag(tag) return EntityRemoveTag(self.ID, tag) end --- ---@param tag string ---@return boolean function NoitaEntity:HasTag(tag) return EntityHasTag(self.ID, tag) end --- ---@return string -- example: 'data/entities/items/flute.xml'. function NoitaEntity:GetFilename() return EntityGetFilename(self.ID) end --- ---@param tag string function NoitaComponent:AddTag(tag) return ComponentAddTag(self.ID, tag) end --- ---@param tag string function NoitaComponent:RemoveTag(tag) return ComponentRemoveTag(self.ID, tag) end --- ---@param tag string ---@return boolean function NoitaComponent:HasTag(tag) return ComponentHasTag(self.ID, tag) end ---Returns one or many values matching the type or subtypes of the requested field. ---Reports error and returns nil if the field type is not supported or field was not found. ---@param fieldName string ---@return any|nil function NoitaComponent:GetValue(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 ... 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. ---Reports error and returns nil if the field type is not supported or 'object_name' is not a metaobject. --- ---Reporting errors means that it spams the stdout with messages, instead of using the lua error handling. Thanks Nolla. ---@param objectName string ---@param fieldName string ---@return any|nil function NoitaComponent:ObjectGetValue(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 ... 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'. ---'table_of_component_values' should be a string-indexed table, where keys are field names and values are field values of correct type. ---The value setting works like ComponentObjectSetValue2(), with the exception that multivalue types are not supported. ---Additional supported values are _tags:comma_separated_string and _enabled:bool, which basically work like the those fields work in entity XML files. ---Returns the created component, if creation succeeded, or nil. ---@param componentTypeName string ---@param tableOfComponentValues table ---@return NoitaComponent|nil function NoitaEntity:EntityAddComponent(componentTypeName, tableOfComponentValues) local componentID = EntityAddComponent2(self.ID, componentTypeName, tableOfComponentValues) if componentID == nil then return nil end return setmetatable({ ID = componentID }, NoitaComponent) end --- ---@param arrayMemberName string ---@param typeStoredInVector "int"|"float"|"string" ---@return number function NoitaComponent:GetVectorSize(arrayMemberName, typeStoredInVector) return ComponentGetVectorSize(self.ID, arrayMemberName, typeStoredInVector) end --- ---@param arrayName 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 --- ---@param arrayName string ---@param typeStoredInVector "int"|"float"|"string" ---@return number[]|number|string|nil function NoitaComponent:GetVector(arrayName, typeStoredInVector) return ComponentGetVector(self.ID, arrayName, typeStoredInVector) end ---Returns true if the given component exists and is enabled, else false. ---@return boolean function NoitaComponent:GetIsEnabled() return ComponentGetIsEnabled(self.ID) end ---Returns a string-indexed table of string. ---@return table|nil function NoitaComponent:GetMembers() return ComponentGetMembers(self.ID) end ---Returns a string-indexed table of string or nil. ---@param objectName string ---@return table|nil function NoitaComponent:ObjectGetMembers(objectName) return ComponentObjectGetMembers(self.ID, objectName) end --- ---@return string string function NoitaComponent:GetTypeName() return ComponentGetTypeName(self.ID) end -- TODO: Add missing Noita API methods and functions. --- ---@param strength number ---@param position Vec2|nil -- Defaults to camera position if not set. function CameraAPI.Screenshake(strength, position) if position == nil then return GameScreenshake(strength) end return GameScreenshake(strength, position.x, position.y) end -- TODO: Add missing Noita API methods and functions. ---Returns the center position of the viewport in world/virtual coordinates. ---@return Vec2 function CameraAPI.Pos() return Vec2(GameGetCameraPos()) end ---Sets the center position of the viewport in world/virtual coordinates. ---@param position Vec2 function CameraAPI.SetPos(position) return GameSetCameraPos(position.x, position.y) end --- ---@param isFree boolean function CameraAPI.SetCameraFree(isFree) return GameSetCameraFree(isFree) end ---Returns the camera boundaries rectangle in world/virtual coordinates. ---This may not be 100% pixel perfect with regards to what you see on the screen. ---@return Vec2 topLeft ---@return Vec2 bottomRight function CameraAPI.Bounds() local x, y, w, h = GameGetCameraBounds() return Vec2(x, y), Vec2(x + w, y + h) end -------------------- -- Noita API root -- -------------------- ---@class NoitaAPI local api = { Component = ComponentAPI, Entity = EntityAPI, Camera = CameraAPI, MetaTables = { Component = NoitaComponent, Entity = NoitaEntity, }, } return api