Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LUA Scripts do not stay after script "Save and Play" #13

Open
manticorp opened this issue Feb 23, 2024 · 2 comments
Open

LUA Scripts do not stay after script "Save and Play" #13

manticorp opened this issue Feb 23, 2024 · 2 comments

Comments

@manticorp
Copy link

Hey! First of all, thanks for the awesome library - I've been using it to load a card game on TTS and it is great so far!

When using Decker to create a deck of cards with a different LUA script on each card, upon doing any other scripting and using the "Save & Play" button in TTS, all objects then get the same script.

To reproduce, use the following script (including Decker at the top):

function reimportCards()
    local cardFaces = 'https://i.imgur.com/wiyVst7.png'
    local cardBack = 'https://i.imgur.com/KQtQGE7.png'

    local cardAsset = Decker.Asset(cardFaces, cardBack, {width = 2, height = 2})

    local cardOne = Decker.Card(cardAsset, 1, 1, {
        Nickname = 'Card A',
        LuaScript = 'card = "A"'
    })

    local cardTwo = Decker.Card(cardAsset, 1, 2, {
        Nickname = 'Card B',
        LuaScript = 'card = "B"'
    })

    local myDeck = Decker.Deck({cardOne, cardTwo})
    myDeck:spawn({position = {0, 3, -6}})
end

function debugCards()
    log('Debugging Cards')
    broadcastToAll('Debugging Cards')
    AllObjectsOnTable = getObjects()
    for k,v in pairs(AllObjectsOnTable) do  --go through all Objects
        if (v.type == "Card") then
            log(v)
            log(v.getName())
            log(v.getVar('card'))
            broadcastToAll('Name: ' .. v.getName() .. ', card: ' .. v.getVar('card'))
        end
    end
end

function onChat(message)
    if message == "reimportCards"
    then
        reimportCards()
    elseif message == "debugCards"
    then
        debugCards()
    end
end

then, without doing anything else, save the game.

Now, reload the game and check the cards - they should have the correct variables in the lua scripts.

Now, reload the game up, and before doing anything, click "Save & Play" in the script editor.

Now check the cards, the Lua scripts will both read card = "A"

It is expected that the cards would read card = "A" and card = "B"

@manticorp
Copy link
Author

I believe this has to do with every object being given the same GUID on creation:

-- Create a new CustomDeck asset
function Decker.Asset(face, back, options)
    local asset = {}
    options = options or {}
    asset.data = {
        FaceURL = face or error('Decker.Asset: faceImg link required'),
        BackURL = back or error('Decker.Asset: backImg link required'),
        NumWidth = options.width or 1,
        NumHeight = options.height or 1,
        BackIsHidden = options.hiddenBack or false,
        UniqueBack = options.uniqueBack or false
    }
    -- Reuse ID if asset existing
    asset.id = assetID(asset.data)
    return setmetatable(asset, assetMeta)
end
-- Pull a Decker.Asset from card JSONs CustomDeck entry
local function assetFromData(assetData)
    return setmetatable({data = assetData, id = assetID(assetData)}, assetMeta)
end

-- Create a base for JSON objects
function Decker.BaseObject()
    return {
        Name = 'Base',
        Transform = {
            posX = 0, posY = 5, posZ = 0,
            rotX = 0, rotY = 0, rotZ = 0,
            scaleX = 1, scaleY = 1, scaleZ = 1
        },
        Nickname = '',
        Description = '',
        Value = 0,
        Tags = {},
        ColorDiffuse = { r = 1, g = 1, b = 1 },
        Locked = false,
        Grid = true,
        Snap = true,
        Autoraise = true,
        Sticky = true,
        Tooltip = true,
        GridProjection = false,
        Hands = true,
        XmlUI = '',
        LuaScript = '',
        LuaScriptState = '',
        GUID = 'deadbf'
    }
end
-- Typical paramters map with defaults
local commonMap = {
    name   = {field = 'Nickname',    default = ''},
    value   = {field = 'Value',    default = 0},
    tags   = {field = 'Tags',   default = {}},
    desc   = {field = 'Description', default = ''},
    script = {field = 'LuaScript',   default = ''},
    xmlui  = {field = 'XmlUI',       default = ''},
    scriptState = {field = 'LuaScriptState', default = ''},
    locked  = {field = 'Locked',  default = false},
    tooltip = {field = 'Tooltip', default = true},
    guid    = {field = 'GUID',    default = 'deadbf'},
    hands   = {field = 'Hands',   default = true},
}

the guid being 'deadbf' - which I read as "dead boyfriend"?? Nonetheless...

My solution has been to generate a new GUID for each object, something akin to the following:

math.randomseed(os.time())
local function randStr(k)
    local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    local n = string.len(alphabet)
    local pw = {}
    for i = 1, k
    do
        pw[i] = string.byte(alphabet, math.random(n))
    end
    return string.char(table.unpack(pw))
end

local function newGuid()
    return randStr(6)
end

-- Create a new CustomDeck asset
function Decker.Asset(face, back, options)
    local asset = {}
    options = options or {}
    asset.data = {
        FaceURL = face or error('Decker.Asset: faceImg link required'),
        BackURL = back or error('Decker.Asset: backImg link required'),
        NumWidth = options.width or 1,
        NumHeight = options.height or 1,
        BackIsHidden = options.hiddenBack or false,
        UniqueBack = options.uniqueBack or false,
        GUID = newGuid()
    }
    -- Reuse ID if asset existing
    asset.id = assetID(asset.data)
    return setmetatable(asset, assetMeta)
end
-- Pull a Decker.Asset from card JSONs CustomDeck entry
local function assetFromData(assetData)
    return setmetatable({data = assetData, id = assetID(assetData)}, assetMeta)
end

-- Create a base for JSON objects
function Decker.BaseObject()
    return {
        Name = 'Base',
        Transform = {
            posX = 0, posY = 5, posZ = 0,
            rotX = 0, rotY = 0, rotZ = 0,
            scaleX = 1, scaleY = 1, scaleZ = 1
        },
        Nickname = '',
        Description = '',
        Value = 0,
        Tags = {},
        ColorDiffuse = { r = 1, g = 1, b = 1 },
        Locked = false,
        Grid = true,
        Snap = true,
        Autoraise = true,
        Sticky = true,
        Tooltip = true,
        GridProjection = false,
        Hands = true,
        XmlUI = '',
        LuaScript = '',
        LuaScriptState = '',
        GUID = newGuid()
    }
end
-- Typical paramters map with defaults
local commonMap = {
    name   = {field = 'Nickname',    default = ''},
    value   = {field = 'Value',    default = 0},
    tags   = {field = 'Tags',   default = {}},
    desc   = {field = 'Description', default = ''},
    script = {field = 'LuaScript',   default = ''},
    xmlui  = {field = 'XmlUI',       default = ''},
    scriptState = {field = 'LuaScriptState', default = ''},
    locked  = {field = 'Locked',  default = false},
    tooltip = {field = 'Tooltip', default = true},
    guid    = {field = 'GUID',    default = 'deadbf'},
    hands   = {field = 'Hands',   default = true},
}
-- Apply some basic parameters on base JSON object
function Decker.SetCommonOptions(obj, options)
    options = options or {}
    for k,v in pairs(commonMap) do
        -- can't use and/or logic cause of boolean fields
        if options[k] ~= nil then
            obj[v.field] = options[k]
        else
            obj[v.field] = v.default
        end
    end
    -- passthrough unrecognized keys
    for k,v in pairs(options) do
        if not commonMap[k] then
            obj[k] = v
        end
    end
    obj.GUID = newGuid()
end

I'm not sure what implications this has for TTS - e.g. what if a duplicate GUID is generated? The chances of this are very low (there are 1.3537087e+16 combinations of 6 digit chars) - but I'm not sure how the GUIDs are generated internally, and whether re-seeding math.random is bad?

I'm not a LUA scripter!

@tjakubo
Copy link
Owner

tjakubo commented Feb 23, 2024

Hey, thanks for the kind words and nice find, I did not expect TTS to mess with unrelated object fields because of duplicate GUIDs :)

The deadbf fixed GUID was just "anything" inspired by typical 32 bit dummy address 0xDEADBEEF (https://en.wikipedia.org/w/index.php?title=Magic_number_(programming)&useskin=vector#DEADBEEF). Idea was, let this GUID be duplicate so TTS reassigns it to a free one automatically when spawning - that's what happens when you for example copy-paste any object manually, or draw from an infinite bag, without scripting.

But since it seems to be problematic, I'll add random GUID generation - this will mean it shouldn't clash with any other GUIDs, and won't be a guarantee... but like you said, it's going to be a very slim chance.
As far as seeding goes, Moonsharp (the library TTS uses to handle scripting) does that for us, so it shouldn't be a problem.

By the way, an easier way of generating a random GUID is just rolling a random number in its range (16 chars in 6 spots, so 16^6 which is equal to 2^24) and converting it to hex string. Uppercase/lowercase should not make a difference there.

function random_guid()
  local value = math.random(0, 2^24)
  return string.format('%x', value)
end

Thanks for the report, I'll fix that and add a test for this case sometime next week as I'm out for the weekend :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants