Dzsozi (h03) Posted October 27 Share Posted October 27 Hi, I am doing a point of interest system, where it is possible to create literal "points" of interest to draw on the screen and interact with. Everything I did works as I wanted to, except one thing where the interactionFunction doesn't get saved in the object, therefore I can't execute it for some reason. I did the object creation system inside a shared script to be able to create POIs both client and server side, and I want to code additional client and server side handling for when to bind and execute the interactionFunction. Here are my codes so far, G_InterestPoint.lua: INTERACTION_KEY = "e" -------------------------------------------------------------------------- -------------------------------------------------------------------------- -------------------------------------------------------------------------- PointOfInterest = {} PointOfInterest.__index = PointOfInterest TBL_POIS = {} function getPointsOfInterest() return TBL_POIS end function PointOfInterest:create(x, y, z, interactionRadius, interactionFunction, visibleDistance, interior, dimension, renderOffsetX, renderOffsetY, renderOffsetZ, onFootOnly) if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then error("Bad argument @ PointOfInterest:create [Expected number at argument 1, 2, 3, got "..type(x)..", "..type(y)..", "..type(z).."]", 2) end if type(interactionFunction) ~= "function" then error("Bad argument @ PointOfInterest:create [Expected function at argument 5, got "..type(interactionFunction).."]", 2) end local poi = {} setmetatable(poi, PointOfInterest) poi.x = x poi.y = y poi.z = z poi.interactionRadius = type(interactionRadius) == "number" and interactionRadius or 1 poi.collision = createColSphere(x, y, z, poi.interactionRadius) poi.collision:setID("poi") -- Set the ID to "poi" to identify it as a point of interest poi.interactionFunction = function(...) return interactionFunction(...) end poi.visibleDistance = type(visibleDistance) == "number" and visibleDistance or 10 poi.interior = type(interior) == "number" and interior or 0 poi.dimension = type(dimension) == "number" and dimension or 0 poi.renderOffsetX = type(renderOffsetX) == "number" and renderOffsetX or 0 poi.renderOffsetY = type(renderOffsetY) == "number" and renderOffsetY or 0 poi.renderOffsetZ = type(renderOffsetZ) == "number" and renderOffsetZ or 0 poi.onFootOnly = (type(onFootOnly) == "boolean" and onFootOnly) or false TBL_POIS[poi.collision] = poi return poi end function PointOfInterest:destroy() TBL_POIS[self.collision] = nil destroyElement(self.collision) return true end function PointOfInterest:getPlayersInRadius() local players = {} for i, player in ipairs(getElementsWithinColShape(self.collision, "player")) do if player.interior == self.interior and player.dimension == self.dimension then table.insert(players, player) end end return players end function PointOfInterest:isPlayerInRadius(player) if not isElement(player) then error("Bad argument @ PointOfInterest:isPlayerInRadius [Expected element at argument 1, got "..type(player).."]", 2) end if getElementType(player) ~= "player" then error("Bad argument @ PointOfInterest:isPlayerInRadius [Expected player at argument 1, got "..getElementType(player).."]", 2) end if player.interior ~= self.interior or player.dimension ~= self.dimension then return false end return isElementWithinColShape(player, self.collision) end -------------------------------------------------------------------------- -------------------------------------------------------------------------- -------------------------------------------------------------------------- function getPOIFromColShape(colshape) if not isElement(colshape) then error("Bad argument @ getPOIFromColShape [Expected element at argument 1, got "..type(colshape).."]", 2) end if getElementType(colshape) ~= "colshape" then error("Bad argument @ getPOIFromColShape [Expected colshape at argument 1, got "..getElementType(colshape).."]", 2) end return TBL_POIS[colshape] end C_InterestPoint.lua: local CURRENT_POI = nil local function executePOIInteraction(key, state, poi, ...) print(inspect(poi)) poi.interactionFunction(...) return true end addEventHandler("onClientColShapeHit", resourceRoot, function(hitElement, matchingDimension) if getElementType(hitElement) == "player" and matchingDimension then if hitElement == localPlayer then local poi = getPOIFromColShape(source) if poi then CURRENT_POI = poi bindKey(INTERACTION_KEY, "down", executePOIInteraction, CURRENT_POI) print("Key", INTERACTION_KEY, "bound to executePOIInteraction") end end end end) addEventHandler("onClientColShapeLeave", resourceRoot, function(leaveElement, matchingDimension) if getElementType(leaveElement) == "player" and matchingDimension then if leaveElement == localPlayer then if CURRENT_POI then CURRENT_POI = nil unbindKey(INTERACTION_KEY, "down", executePOIInteraction) print("Key", INTERACTION_KEY, "unbound") end end end end) -------------------------------------------------------------------------- -------------------------------------------------------------------------- -------------------------------------------------------------------------- addEventHandler("onClientRender", root, function() for colshape, data in pairs(getPointsOfInterest()) do local distance = getDistanceBetweenPoints3D(data.x, data.y, data.z, localPlayer.position.x, localPlayer.position.y, localPlayer.position.z) local alphaFade = math.min(255, math.max(0, 255 - ((distance - (data.visibleDistance * 0.7)) / (data.visibleDistance * 0.3)) * 255)) if distance <= data.visibleDistance then local sx, sy = getScreenFromWorldPosition(data.x + data.renderOffsetX, data.y + data.renderOffsetY, data.z + data.renderOffsetZ) if sx and sy then local circleColorIn = tocolor(100, 200, 255, alphaFade) local circleColorOut = tocolor(100, 200, 255, alphaFade) if isElementWithinColShape(localPlayer, colshape) then if not (data.onFootOnly and localPlayer.vehicle) then circleColorOut = tocolor(255, 255, 255, alphaFade) end end dxDrawCircle(sx, sy, 8, 0, 360, circleColorOut, circleColorIn, 8, 1, false) end end end end) function testfunc() outputChatBox('You are near the point of interest!') return true end addEventHandler("onClientResourceStart", resourceRoot, function() local test = PointOfInterest:create(195.00665283203, -141.83079528809, 1.5858917236328, 1, testfunc, 10, 0, 0, 0, 0, 1, true) return true end) addEventHandler("onClientResourceStop", resourceRoot, function() return true end) (I don't have server side bind and execution logic done yet) This client side POI is at blueberry behind the pizza stack, between the pizza place and the motel. When I go inside it and press E, it is visible that the poi object doesn't have the interactionFunction variable, and I get an error saying: ERROR: InterestPoint\C_InterestPoint.lua:5: attempt to call field 'interactionFunction' (a nil value) I tried everything, putting the G_InterestPoint.lua from shared to client in the meta, I tried making the poi.interactionFunction as poi.interactionFunction = function(...) return interactionFunction(...) end but none of it works, yields the same error. What is the reason behind this behaviour? Why I can't store the function as a variable through a function exectuion? The strangest thing is that I shouldn't be able to create the POI object at the first place if the interactionFunction were not a function type parameter. Thank you for the help in advance! Link to comment
Moderators IIYAMA Posted October 27 Moderators Share Posted October 27 3 hours ago, Dzsozi (h03) said: What is the reason behind this behaviour? Why I can't store the function as a variable through a function exectuion? The strangest thing is that I shouldn't be able to create the POI object at the first place if the interactionFunction were not a function type parameter. I tested the following code with this compiler and it works. https://onecompiler.com/lua/42wdewngs Are you sure you are not synchronizing the data or exporting it(call/export)? Because in that case interactionFunction becomes nil. PointOfInterest = {} PointOfInterest.__index = PointOfInterest function PointOfInterest:create(x, y, z, interactionRadius, interactionFunction, visibleDistance, interior, dimension, renderOffsetX, renderOffsetY, renderOffsetZ, onFootOnly) if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then error("Bad argument @ PointOfInterest:create [Expected number at argument 1, 2, 3, got "..type(x)..", "..type(y)..", "..type(z).."]", 2) end if type(interactionFunction) ~= "function" then error("Bad argument @ PointOfInterest:create [Expected function at argument 5, got "..type(interactionFunction).."]", 2) end local poi = {} setmetatable(poi, PointOfInterest) poi.x = x poi.y = y poi.z = z poi.interactionRadius = type(interactionRadius) == "number" and interactionRadius or 1 -- [temp disabled] poi.collision = createColSphere(x, y, z, poi.interactionRadius) -- [temp disabled] poi.collision:setID("poi") -- Set the ID to "poi" to identify it as a point of interest poi.interactionFunction = interactionFunction -- [simplified] poi.visibleDistance = type(visibleDistance) == "number" and visibleDistance or 10 poi.interior = type(interior) == "number" and interior or 0 poi.dimension = type(dimension) == "number" and dimension or 0 poi.renderOffsetX = type(renderOffsetX) == "number" and renderOffsetX or 0 poi.renderOffsetY = type(renderOffsetY) == "number" and renderOffsetY or 0 poi.renderOffsetZ = type(renderOffsetZ) == "number" and renderOffsetZ or 0 poi.onFootOnly = (type(onFootOnly) == "boolean" and onFootOnly) or false -- [temp disabled] TBL_POIS[poi.collision] = poi return poi end function testfunc() print('You are near the point of interest!') return true end local test = PointOfInterest:create(195.00665283203, -141.83079528809, 1.5858917236328, 1, testfunc, 10, 0, 0, 0, 0, 1, true) print(test.interactionFunction()) Output: You are near the point of interest! true Link to comment
Dzsozi (h03) Posted October 28 Author Share Posted October 28 Every bit of code I posted above is what I have, additionally here is my current meta.xml to prove my setup. I also tried doing this script without the OOP scripting parts (like using metatable and such) but it still had the same result. The in-game print(inspect(poi)) function call from inside the executePOIInteraction doesn't display the variable and says there's no such thing as interactionFunction in the object. I also used the simplified method in the first place like you have now, but that didn't work either, that's why I somewhat forced the variable to be a function. My current meta.xml (I use a custom python script to generate the boilerplate resource files): <meta> <info author="driphunnid" type="utility" name="InterestPoint" /> <download_priority_group>3</download_priority_group> <min_mta_version client="1.6.0" server="1.6.0" /> <!-- <include resource="" /> --> <!-- a config must be an xml file, types: client or server --> <!-- <config src=".xml" type="client" /> <config src=".xml" type="server" /> --> <oop>true</oop> <!-- <settings> <setting name="" value="" /> </settings> --> <!-- <aclrequest> <right name="function.startResource" access="true" /> <right name="function.stopResource" access="true" /> </aclrequest> --> <!-- if download is false you can use downloadFile to download it later --> <!-- <file src="assets/" download="true" /> --> <script src="G_InterestPoint.lua" type="shared" /> <!-- <export function="" type="shared" /> --> <script src="C_InterestPoint.lua" type="client" /> <!-- <export function="" type="client" /> --> <script src="S_InterestPoint.lua" type="server" /> <!-- <export function="" type="server" /> --> <!-- <sync_map_element_data>false</sync_map_element_data> --> <!-- <map src=".map" dimension="0"/> --> </meta> Just for extra context (or if someone needs something like this) here is the python code used for resource files generation: import os def create_file(path, filename, content): """Create a file with the given content.""" with open(os.path.join(path, filename), 'w') as file: file.write(content) def main(): # Prompt for folder location folder_location = input("Paste the folder location here: ") # Check if the folder exists if not os.path.isdir(folder_location): print("Folder does not exist, please check the path and try again.") return # Get the name of the folder folder_name = os.path.basename(folder_location) # If a meta.xml file already exists, ask if the user wants to overwrite it if os.path.isfile(os.path.join(folder_location, "meta.xml")): overwrite = input("A meta.xml file already exists with possibly other resource files. Do you want to overwrite it? (y/n): ") if overwrite.lower() != "y": print("Operation cancelled.") return print() # Prompt for script type - utility, main, system print("Script types: utility, main, system, other (anything custom, you don't have to use 'other')") print("'utility' download group is 3, 'main' is 2, 'system' is 1, and anything other is 0.") script_type = input("Enter the script type: ") # Set download priority group based on script type if script_type == "utility": download_priority_group = 3 elif script_type == "main": download_priority_group = 2 elif script_type == "system": download_priority_group = 1 else: download_priority_group = 0 # File contents (customize as needed) contents = { "C_" + folder_name + ".lua": ''' -- Implement your script addEventHandler("onClientResourceStart", resourceRoot, function() return true end) addEventHandler("onClientResourceStop", resourceRoot, function() return true end) ''', "S_" + folder_name + ".lua": ''' -- Implement your script addEventHandler("onResourceStart", resourceRoot, function() return true end) addEventHandler("onResourceStop", resourceRoot, function() return true end) ''', "G_" + folder_name + ".lua": '', "meta.xml": ''' <meta> <info author="driphunnid" type="''' + script_type + '''" name="''' + folder_name + '''" /> <download_priority_group>''' + str(download_priority_group) + '''</download_priority_group> <min_mta_version client="1.6.0" server="1.6.0" /> <!-- <include resource="" /> --> <!-- a config must be an xml file, types: client or server --> <!-- <config src=".xml" type="client" /> <config src=".xml" type="server" /> --> <oop>true</oop> <!-- <settings> <setting name="" value="" /> </settings> --> <!-- <aclrequest> <right name="function.startResource" access="true" /> <right name="function.stopResource" access="true" /> </aclrequest> --> <!-- if download is false you can use downloadFile to download it later --> <!-- <file src="assets/" download="true" /> --> <script src="G_''' + folder_name + '''.lua" type="shared" /> <!-- <export function="" type="shared" /> --> <script src="C_''' + folder_name + '''.lua" type="client" /> <!-- <export function="" type="client" /> --> <script src="S_''' + folder_name + '''.lua" type="server" /> <!-- <export function="" type="server" /> --> <!-- <sync_map_element_data>false</sync_map_element_data> --> <!-- <map src=".map" dimension="0"/> --> </meta>''', } # Create files for filename, content in contents.items(): create_file(folder_location, filename, content) print(f"Created {filename}") # Create a folder for the resources, assets, etc. os.makedirs(os.path.join(folder_location, "assets")) print("Created 'assets' folder") if __name__ == "__main__": main() I am not entirely sure what am I missing or doing wrong, I've done similar systems before where I had to pass a function type as parameter for later execution, there were no problems like this previously. And as I wrote, currently the main POI object creation system is located in the G_InterestPoint.lua which is a shared / global script inside the resource, but I tried changing it to client side to see if that is the cause of the problem, but no. Link to comment
Moderators IIYAMA Posted October 28 Moderators Share Posted October 28 13 hours ago, Dzsozi (h03) said: but I tried changing it to client side to see if that is the cause of the problem, but no. Have you used debug lines to debug/verify every part of the call chain? Link to comment
Dzsozi (h03) Posted October 28 Author Share Posted October 28 39 minutes ago, IIYAMA said: Have you used debug lines to debug/verify every part of the call chain? What do you mean exactly? I just tested the code at this moment, as you already know, the system is in a shared script, and I create the test POI at client side, so far this works. I added an extra debug print in the PointOfInterest:create method: poi.interactionFunction = interactionFunction -- simplified print(type(poi.interactionFunction)) -- debugscript 3 says: function - note that I shouldn't be able to create the POI if it were not a function... poi.interactionFunction() -- chatbox says: "You are near the point of interest!" as the test poi is created onClientResourceStart Other than that I have a debug print for this function - located on client side: local function executePOIInteraction(key, state, poi, ...) print(inspect(poi)) poi.interactionFunction(...) return true end I also added a debug part when the colshape is hit on client side and getPOIFromColShape is used: addEventHandler("onClientColShapeHit", resourceRoot, function(hitElement, matchingDimension) if getElementType(hitElement) == "player" and matchingDimension then if hitElement == localPlayer then local poi = getPOIFromColShape(source) outputConsole(inspect(poi)) -- added for debug purposes if poi then if poi.onFootOnly and hitElement.vehicle then return end CURRENT_POI = poi bindKey(INTERACTION_KEY, "down", executePOIInteraction, CURRENT_POI) print("Key", INTERACTION_KEY, "bound to executePOIInteraction") end end end end) When I hit the colshape it says this: restart: Resource restarting... You are near the point of interest! { collision = elem:colshape2C61A570, dimension = 0, interactionFunction = <function 1>, -- ?????? interactionRadius = 1, interior = 0, onFootOnly = true, renderOffsetX = 0, renderOffsetY = 0, renderOffsetZ = 1, visibleDistance = 10, x = 195.00665283203, y = -141.83079528809, z = 1.5858917236328, <metatable> = <1>{ __index = <table 1>, create = <function 2>, destroy = <function 3>, getPlayersInRadius = <function 4>, isPlayerInRadius = <function 5> } } Clearly it is there, but why is it not when I use the key bind? The executePOIInteraction function is to use with the bindKey function when the player enters the colshape - however when this executes (I press "e" inside the colshape) it displays this: { collision = elem:colshape344AB8D8, dimension = 0, interactionRadius = 1, interior = 0, onFootOnly = true, renderOffsetX = 0, renderOffsetY = 0, renderOffsetZ = 1, visibleDistance = 10, x = 195.00665283203, y = -141.83079528809, z = 1.5858917236328 } and I get the error that I've been having; ERROR: InterestPoint\C_InterestPoint.lua:5: attempt to call field 'interactionFunction' (a nil value) What kind of other debug should I consider, it clearly fails unexpectedly for some reason and I can't figure out why where and when. When I restart the resource, you can see in the console that the testfunc is being executed properly, but not after, on demand, using the key bind. Link to comment
Moderators IIYAMA Posted October 28 Moderators Share Posted October 28 4 hours ago, Dzsozi (h03) said: Clearly it is there, but why is it not when I use the key bind? When are using the following: bindKey timers trigger(server/client)Event (set/get)ElementData etc... The data that you pass through these functions is being cloned. The reason behind that is: the data is leaving it's current Lua environment. With as goal to maintain the data, else it is lost. Unfortunately metatables and functions can't be cloned, which is why you did encounter data loss. Fortunately, because they can't be cloned you know that something is wrong. If they were be able to get cloned, you would have ended up with multiple clones of the same entity that do not share data. Which becomes really hard to debug. To resolve this issue, do not pass the element but use a self made id(integer). And make it look up able by using another table. Link to comment
Dzsozi (h03) Posted October 29 Author Share Posted October 29 2 hours ago, IIYAMA said: The data that you pass through these functions is being cloned. Ahh, you are right, I totally forgot about this, I have had similar problems before because of this cloning behaviour. 2 hours ago, IIYAMA said: And make it look up able by using another table. I get it that instead I should use an integer id, but previously in other systems where I did that, I got into so much work and big messy situations, because when I delete an existing POI dynamically (by script) for example I delete POI id 1, but there are other POIs with higher ids, then the whole table gets shifted down by one id and I can't use the same id to refer to the same POI for example with id 3 (because it becomes id 2 if i use table.remove), if the ids are static then it means that I should only make the id grow, then eventually the id would reach massive numbers like 213821, which I would like to avoid for the sake of simplicity. I could maybe use onClientKey I assume, but then how would I call the server side POIs interactionFunction? The interactionFunctions might also need parameters in some case like passing the player, for an inventory system, when a player interacts with a POI, give an item to the player, so on. What do you recommend, should I go with infinitely growing ID numbers which will reset only on resource restart or try to rework the whole POI system to be used with onClientKey? For some reason infinitely growing IDs seem unoptimized and unethical to me, that's why I did the collision element as an index, they are easy to look up and refer to based on a simple element. Also I don't really get what do you mean by making it lookupable by using another table ? Link to comment
Moderators IIYAMA Posted October 29 Moderators Share Posted October 29 17 hours ago, Dzsozi (h03) said: then eventually the id would reach massive numbers like 213821, which I would like to avoid for the sake of simplicity. 17 hours ago, Dzsozi (h03) said: For some reason infinitely growing IDs seem unoptimized It is just an integer, 213821 nothing in terms of memory. No matter how long the number is, it will not use more memory than if it was a string. 10000000000 consumes less memory than "10000000000". If you do not believe me, ask Chat GPT... Also, I asked Chat GPT the following: How many times can fit 10000000000 in 8gb of memory Quote Let's figure this out! Understanding the Units 1 GB (Gigabyte) = 1,073,741,824 bytes 8 GB = 8 * 1,073,741,824 bytes = 8,589,934,592 bytes We're assuming 64-bit integers, which take 8 bytes each. Calculation Divide total memory by the size of each integer: 8,589,934,592 bytes / 8 bytes/integer = 1,073,741,824 integers. Answer: You can fit approximately 1,073,741,824 integers of the value 10000000000 into 8 GB of memory. Important Note: This calculation assumes you're only storing these integers. If you're using other data structures, variables, or running a program, the available space will be reduced. 17 hours ago, Dzsozi (h03) said: (because it becomes id 2 if i use table.remove) table.remove is only useful when you want to use a table as an array. It is very useful if order matters, but in your case there is no need for maintaining an order. It might save some memory, but the look up speed will consumes (incremental) a lot more CPU(you need to loop) which is a big trade off. local temp = {1, 2, 3, 7, 10, 100} -- id's inside 17 hours ago, Dzsozi (h03) said: Also I don't really get what do you mean by making it lookupable by using another table ? For example the table pointOfInterestCollection. do local id = 0 ---@return integer function generateId () id = id + 1 return id end end ---@type {[integer]: table|nil} local pointOfInterestCollection = {} ---@param poi table function savePointOfInterest (poi) local id = generateId () poi.id = id pointOfInterestCollection[id] = poi end ---@param id integer ---@return table|nil function loadPointOfInterest(id) return pointOfInterestCollection[id] end ---@param id integer ---@return boolean function removePointOfInterest(id) if not pointOfInterestCollection[id] then return false end pointOfInterestCollection[id] = nil return true end Link to comment
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now