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

🔨 Smarter "Module Stacking" and reload #3300

Open
3 tasks done
zach2good opened this issue Nov 30, 2022 · 5 comments
Open
3 tasks done

🔨 Smarter "Module Stacking" and reload #3300

zach2good opened this issue Nov 30, 2022 · 5 comments
Assignees
Labels
enhancement New feature request

Comments

@zach2good
Copy link
Contributor

I affirm:

  • I understand that if I do not agree to the following points by completing the checkboxes my issue will be ignored.
  • I have read and understood the Contributing Guide and the Code of Conduct.
  • I have searched existing issues to see if the issue has already been opened, and I have checked the commit log to see if the issue has been resolved since my server was last updated.

Describe the feature

Currently, when we load a module and its constituent overrides, each override grabs its target function, stashes it "internally" to itself as super and then replaces it. This is a totally one-way process.

As discussed here, it was mentioned that events can sometimes (or all the time) be activated/deactivated without a server restart.

I'm still 1000% on board to implement all seasonal events as modules, I think the benefits far outweigh any drawbacks from it being a bit more awkward to deal with.

This will require a couple of additional features though:

Module setEnabled true/false, through hot reload:

If we have a couple of modules, overriding the same function:

xi.global.exampleFunction(arg)
    - module_1_override
        - module_2_override
            - module_3_override

We have no way currently of removing module 1 or 2's override, without clobbering everything else. I also think that additional hot-reloads of modules might apply the override multiple times? (Which is why you shouldn't be using hot-reload on your live servers, folks!).

We need core to be able to hot-reload a module, and as soon as it sees the hot-reload, it goes through all of it's previous overrides and removes them from their host functions, leaving the host and other overrides intact. It can then go through its regular load cycle and apply its own overrides.

Some sort of mapping system would be the goal here.

Module hooks for onInit and onDeinit

If we're going to be enabling and disabling modules at runtime, we need hooks for these actions. The use case being:

onModuleInit: generate dynamic npcs for festive zones, override zone music, add more stuff to existing npcs
onModuleDeinit: remove all of that stuff, leaving everything else cleanly in place

Exactly where this would happen would be unknown, since a lot of things require the zone to be not-yet populated, and a lot of things do.

But this is the sort of direction I think we should be heading in. Limited-time event code shouldn't exist alongside the regular day-to-day server code. There shouldn't be global settings for it. It should all be self-contained in event modules.

@zach2good
Copy link
Contributor Author

Pop module:

function popModule(full_func_name)
    -- Split full_func_name by '.'
    local parts = {}
    for part in string.gmatch(full_func_name, "[^%.]+") do
        table.insert(parts, part)
    end

    -- Look up the functions from _G, so we can capture base_table and name
    local base_table = _G
    for i = 1, #parts - 1 do
        base_table = base_table[parts[i]]
        if not base_table then
            print("Error: Invalid module path: " .. full_func_name)
            return
        end
    end

    -- Collect current function
    local name = parts[#parts]
    local current = base_table[name]
    if not current then
        print("Error: Function " .. name .. " does not exist in " .. full_func_name)
        return
    end
    
    local current = base_table[name]
    local env = getfenv(current)
    
    -- Replace
    if env.super then
        base_table[name] = env.super
    else
        print("No super function to restore.")
    end
end

@zach2good
Copy link
Contributor Author

if you ever need to use fenv stuff in higher versions of Lua (online compilers/interpreters) where it isn't available, this snippet re-implements them:

if not setfenv then -- Lua 5.2+
  local debug = require("debug")
  -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html
  -- this assumes f is a function
  local function findenv(f)
    if not debug.getupvalue then return nil end
    local level = 1
    repeat
      local name, value = debug.getupvalue(f, level)
      if name == '_ENV' then return level, value end
      level = level + 1
    until name == nil
    return nil end
  getfenv = function (f) return(select(2, findenv(f)) or _G) end
  setfenv = function (f, t)
    local level = findenv(f)
    if level then debug.setupvalue(f, level, t) end
    return f end
end

@zach2good
Copy link
Contributor Author

Important to remember that a module is just a bag of overrides

@zach2good
Copy link
Contributor Author

if not setfenv then -- Lua 5.2+
  local debug = require("debug")
  -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html
  -- this assumes f is a function
  local function findenv(f)
    if not debug.getupvalue then return nil end
    local level = 1
    repeat
      local name, value = debug.getupvalue(f, level)
      if name == '_ENV' then return level, value end
      level = level + 1
    until name == nil
    return nil end
  getfenv = function (f) return(select(2, findenv(f)) or _G) end
  setfenv = function (f, t)
    local level = findenv(f)
    if level then debug.setupvalue(f, level, t) end
    return f end
end

function split(str, pattern)
    local parts = {}
    for part in string.gmatch(str, pattern) do
        table.insert(parts, part)
    end
    return parts
end

function pushFunction(full_func_name, func)
    -- Split full_func_name by '.'
    local parts = split(full_func_name, "[^%.]+")

    -- Look up the functions from _G, so we can capture base_table and name
    local base_table = _G
    for i = 1, #parts - 1 do
        base_table = base_table[parts[i]]
        if not base_table then
            print("Error: Invalid module path: " .. full_func_name)
            return
        end
    end

    -- Collect current function
    local name = parts[#parts]
    local current = base_table[name]
    if not current then
        print("Error: Function " .. name .. " does not exist in " .. full_func_name)
        return
    end
    
    local old = base_table[name]

    local thisenv = getfenv(old)

    local env = { super = old }
    setmetatable(env, { __index = thisenv })

    setfenv(func, env)

    base_table[name] = func
end

function popFunction(full_func_name)
    -- Split full_func_name by '.'
    local parts = split(full_func_name, "[^%.]+")

    -- Look up the functions from _G, so we can capture base_table and name
    local base_table = _G
    for i = 1, #parts - 1 do
        base_table = base_table[parts[i]]
        if not base_table then
            print("Error: Invalid module path: " .. full_func_name)
            return
        end
    end

    -- Collect current function
    local name = parts[#parts]
    local current = base_table[name]
    if not current then
        print("Error: Function " .. name .. " does not exist in " .. full_func_name)
        return
    end
    
    local current = base_table[name]
    local env = getfenv(current)
    
    -- Replace
    if env.super then
        base_table[name] = env.super
    else
        print("No super function to restore.")
    end
end

xi = {}
xi.test = {}

xi.test.func = function()
    print('Original function')
end

print('1')
xi.test.func()

pushFunction('xi.test.func', function()
    print('New function!')
end)

print('2')
xi.test.func()

popFunction('xi.test.func')

print('3')
xi.test.func()
1
Original function
2
New function!
3
Original function

@zach2good
Copy link
Contributor Author

Modules being bags of overrides: we don't really store anything on the Core side apart from a big fat bag of overrides. We'll need a global registry of what lives where, so we can do lookups of modules by name so we can enable/disable them.

Then when we do lookup of "some_module" it'll go to the registry, look it up, find which overrides are associated with it, and then use whatever mechanism is required to go to each override and pull it out of the "stack".

I guess the problem is then re-applying it will apply it at the end of the stack, so it could be in a different order than the original.

So, we do need an enable/disable, rather than an apply/remove

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

No branches or pull requests

1 participant