Tutorial; Sharing State Between Scopes - HWRM/KarosGraveyard GitHub Wiki

The problem

There is no way to share any variables or state with other Lua scopes which may be running alongside the current one (outside sharing single numbers using UI_SetElementCustomData / UI_GetElementCustomData).

For exmaple, a script running the mission rules will be running during gametime, and so will ship customcode scripts, but they have no way to communicate with eachother!.

This is because they run in different 'scopes', meaning their Lua environments are seperate:

-- my_level_script.lua

SOME_GLOBAL = 10;
-- elsewhere.lua

print(SOME_GLOBAL); -- nil

In order to overcome this, we can write to the UI in a very specific way and keep a string representation of a lua table there!

Implementation

-- scripts/state_scope.lua

tbl_util = {};

function tbl_util:merge(tbl_a, tbl_b, merger)
    merger = merger or function (a, b)
        if (type(a) == "table" and type(b) == "table") then
            return %self:merge(a, b);
        else
            return (b or a);
        end
    end
    if (tbl_a == nil and tbl_b ~= nil) then
        return tbl_b;
    elseif (tbl_a ~= nil and tbl_b == nil) then
        return tbl_a;
    elseif (tbl_b == nil and tbl_b == nil) then
        return {};
    end
    local out = {};
    -- basic copy
    for k, v in tbl_a do
        out[k] = v;
    end
    for k, v in tbl_b do
        if (out[k] == nil) then
            out[k] = v;
        else
            out[k] = merger(out[k], v);
        end
    end
    return out;
end


function makeStateHandle(screen_name, dropdown_host_el)
    screen_name = screen_name or "DefaultStateScreen";
    dropdown_host_el = dropdown_host_el or "host_dropdown";

    if (UI_GetElementCustomData(screen_name, dropdown_host_el) ~= 1) then
        UI_AddDropDownListboxItem(screen_name, dropdown_host_el, "_", "", 0, "{}");
        UI_SetElementCustomData(screen_name, dropdown_host_el, 1); -- 1 = init
    end

    return function(new_state, overwrite)
        UI_SelectDropDownListboxItemIndex(%screen_name, %dropdown_host_el, 0);
        local current_state = dostring("return " .. (UI_GetDropdownListBoxSelectedCustomDataString(%screen_name, %dropdown_host_el) or "{}"));

        if (new_state) then
            UI_ClearDropDownListbox(%screen_name, %dropdown_host_el);

            if (overwrite) then
                current_state = new_state;
            else
                current_state = tbl_util:merge(current_state, new_state);
            end

            local asStr = function (v, tblParser)
                if (type(v) == "table") then
                    local out = "{";
                    for k, v in v do
                        local i = tostring(k);
                        if (type(k) == "number") then
                            i = "[" .. k .. "]";
                        end
                        out = out .. i .. "=" .. tblParser(v, tblParser) .. ",";
                    end
                    out = out .. "}";
                    return out;
                elseif (type(v) == "string") then
                    return "\"" .. v .. "\"";
                else
                    return tostring(v);
                end
            end

            local state_str = asStr(current_state, asStr);
            UI_AddDropDownListboxItem(%screen_name, %dropdown_host_el, "_", "", 0, state_str);
        end

        return current_state;
    end
end

The host screen:

-- ui/newui/defaultstatescreen.lua

DefaultStateScreen = {
    size = { 0, 0, 0, 0 }
    ;
    {
        type = "DropDownListBox",
        name  = "host_dropdown",
        ListBox = {
            type = "ListBox",
            ItemToClone = {
                type = "TextListBoxItem"
            },
        }
    }
};

You also need to register the screen in uisettings.lua (which is a stock file, make sure to paste what's in here first and just add a new entry):

{
    name = "DefaultStateScreen",
    filename = "data:\\ui\\newui\\defaultstatescreen.lua",
    activated = 0
},

Usage

-- some scope, perhaps:
-- leveldata/campaign/my_campaign/my_mission.lua

dofilepath("data:scripts/state_scope.lua");

local state = makeStateHandle();
state({ a = "foo", b = 10, c = { d = 1 }});
-- some other scope, i.e:
-- ships/hgn_mothership/hgn_mothership.lua

dofilepath("data:scripts/state_scope.lua");

local state = makeStateHandle();

-- if this code runs after the code in the above script (in terms of gametime), we can access the state we wrote:
local s = state();

print(s.a); -- 'foo'
print(s.b); -- '10'
print(tostring(s.c)); -- table: <table address>
print(s.c.d); -- '1'

You can get and set the state from any runtime scope (any scope with access to UI_ calls).

Remember that global state can get messy quickly since many parts of your code may potentially modify it. Additionally, we are writing and reading from this 'superglobal' table asynchronously, meaning someone may edit the table in-between us setting and reading!


By Novaras aka Fear