Jump to content

Function doesn't get saved as a table variable


Recommended Posts

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
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

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
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
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)

spacer.png

 

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
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
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
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

  1. 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

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...