TheOpenSpaceProgram / new-ospgl

A space exploration game in OpenGL. Devblog: https://tatjam.github.io/index.html
MIT License
42 stars 6 forks source link

Think and develop the patching system #35

Open tatjam opened 1 year ago

tatjam commented 1 year ago

FILE REPLACEMENTS: Useful for models, textures, shaders, etc... Mods may specify certain files to be "replaced" (virtually) by other. This includes .lua files and everything else. An example of how it could look like. This code would be executed during loading of the mod:

assets.replace("core:shaders/atmosphere.vs", "ourmod:shaders/atmosphere.vs")
assets.replace("core:shaders/atmosphere.fs", "outmod:shaders/atmosphere.fs")

What happens if two mods replace the same file? We may allow the mods or user to specify a load order for individual files which have conflicts, or for the mods themselves to specify a default ordering for certain files. (This allows the concept of a mod which makes two other mods compatible by adjusting the load orders!)


TOML PATCHES: Useful for config tunings

This is easy to implement and would allow patches to be applied to toml files, thus allowing easy alteration of configuration files. We need to think of a way to establish order of execution of these "patch scripts", maybe something like:

-- (bigger_nozzles:bigger_nozzles.lua)
-- the second string is lua code, executed with "root" as the loaded toml file
assets.patch("test_parts:parts/engine/part_engine.toml", "root.machine[0].exit_radius = 4.0", {before="x2_nozzles"})
-- (realistic_bigger_nozzles:realistic_bigger_nozzles.lua)
assets.patch("test_parts:parts/engine/part_engine.toml", 
    "root.machine[0].exit_radius = root.machine[0].exit_radius * 0.65", 
    {after = ["bigger_nozzles"], before = ["x2_nozzles"]})

This would result in a nozzle size of 4.0 (bigger_nozzles, first) * 0.65 (realistic_bigger_nozzles) * 2.0 (x2_nozzles, last) if all mods were loaded. Impossible conditions could be detected, and dealt with accordingly. (Error message and maybe not executing the patch) In this example, if bigger_nozzles was not present, realistic_bigger_nozzles would not be applied, as it explicitly states that it must execute AFTER bigger_nozzles.

This example also showcases how modders may make their mod compatible with another, without needing the other mod to change its code: "x2_nozzles" may blindly patch everything in this case.


LUA PATCHES: These should rarely be needed if mods are properly designed with configuration in mind

This can be handled by manually hooking / changing functions from lua (only for public functions) or completely replacing files with replace (if you need to change something local). Furthermore, we could implement a proper lua patching system (ie, it works in C++) which would affect a script every time it's loaded. Once more, we can only access public functions with this method:

-- (mod_1:machine.lua)
public_variable = 54
function do_something_public() ... end
-- (mod_2:patch.lua)
assets.hook_lua("mod_1:machine.lua", "do_something_public", our_function, {after:"mod_1"})
assets.replace_lua("mod_1:machine.lua", "do_something_public", other_function)
-- For a variable we want to control externally, we can use the power of metatables:
assets.replace_lua("mod_1:machine.lua", "public_variable", table_with_metamethods)

This exploits the fact that everything runs in the same lua context so it would NOT work for patching planet surface generation files!

Here, replacements conflicts would be handled in the same way as "replace" conflicts, and hooks use a similar conflict resoltion syntax to toml patches.

tatjam commented 1 year ago

It would also be interesting to be able to run a ModuleManager inspired script for all toml files loaded. This script could intelligently inspect all toml files to patch generic stuff. For example, x2_nozzles could be implemented as (pseudocode)

function patch(toml, type)
    if type ~= "part" then return end
    for machine in toml.get_array_of_table("machine") do
       if machine.script == "core:machines/engine/machine.lua" then
            machine.exit_radius = machine.exit_radius * 2.0
       end
    end
end
assets.add_global_patch(patch, {after: "core"})

This requires giving TOML files a context, this may be done easily. Note the explicit after: "core", as we depend on a machine from "core" being present, it would not make sense to run this code if "core" is not present. Not that it matters, as "core" must always be present.