Ruin0x11 / OpenNefia

(Archived) Moddable engine reimplementation of the Japanese roguelike Elona.
MIT License
116 stars 18 forks source link

Namespace custom game object fields by the mod that adds them #15

Closed Ruin0x11 closed 3 years ago

Ruin0x11 commented 4 years ago

Many games like Minecraft have systems where mods can attach custom data to game objects. In that game specifically, groups of custom fields are placed in tagged bundles, and mods check if the object has a specific bundle if they want to use fields from other mods.

This is in contrast to how OpenNefia currently handles custom fields added by mods, in which... it doesn't. Mods simply add whatever data they want directly on the object. This can cause numerous issues:

There could be multiple ways of handling these issues:

  1. Keep a centralized repository of fields added by mods using the existing schema system, and warn the author if they try to publish a mod that causes conflicts. This means mod authors have to manually declare what fields they could add to data types.
  2. Force mods to add new fields on a subfield of game objects, like object.m.my_mod.field. This increases verbosity, but makes it completely unambiguous which mod the field is concerned with and eliminates concerns about naming conflicts. After this is done, __newindex on the game object can be overridden to make it impossible to add new fields directly on the object's table.
Ruin0x11 commented 3 years ago

I think we've decided on the ModExtTable paradigm. It's not fully realized yet, but maybe you could declare a new _ext table on each definition:

data:add {
   _type = "base.chara",
   _id = "spiral_putit",

   _ext = {
      moar_putits = {
         putit_class = "deadly"
      }
   }
}

And have some way of adding to _ext after something was already defined, using transactional data edits.

Data.add_mod_extension("moar_putits", "elona.putit", { putit_class = "benign" })

The edits must be wrapped in callbacks to be compatible with hotloading, else you'd have to restart the engine to get the data mutated in the correct order. This would use an API similar to that of Event.register(), and function in a very similar way. (And event callbacks are compatible with hotloading, so there's prior art.) If we replace one transactional edit then all you'd have to do is take the original data and run all the edits in order to arrive at the final data you want, no restart required.

If we don't do this, data definitions won't be compatible with hotloading, so we have to implement it before stability is reached.

Ruin0x11 commented 3 years ago

NB: Read up on Forge's capability system. Could have some good ideas, as opposed to just shoving all the mod extended data into an unstructured table.

Ruin0x11 commented 3 years ago

I think the capabilities should be interfaces, just like the base engine's. They could get passed their own private table somehow for the :init() and similar methods.

local visual_ai_plan = chara:get_capability(IVisualAICapable)
if visual_ai_plan then
   visual_ai_plan:run(chara)
end

And we'd have a :get_or_create_capability(iface) method that also checks if the interface implements ICapability and validates that the thing (map object or other IModExtendable) can receive the capability.

Ruin0x11 commented 3 years ago

I think it would be very convenient to specify items as acting like an armor/weapon by merely defining a capability. The way armor and weapons are defined currently is a bit hackish. The capability would store the DV/PV or dice rolls of the object.

Ruin0x11 commented 3 years ago

Maybe it would also be best to just replace the params table for items with capabilities. That way you can combine capabilities to create a new item that "acts like" something like a blacksmith hammer, but is also wieldable as a weapon, without tying the hammer's behavior to its item ID.