OK, so here's a problem I have, in simplified version. There's a game, and I want to build a bunch of mods for it. Mod builder looks like this:
class FooMod < ModBuilder # tons of methods for different modifications and analyses def build_mod_files! # various commands to transform game file and create new ones end end FooMod.new("game/path").build!("build/path")
Where ModBuilder is base class, instantiated with path to base game, then we call #build! telling it where it should output generated mod. #build! in base class does all the boring admin stuff and calls #build_mod_files! in subclass to do all the stuff that's specific to particular mod.
This pattern has been quite successful for me - you can check a lot of Crusader Kings 2 and Europa Universalis 4 mods of various sizes built I made with it in this repository. There's also similar code for some other games like Medieval 2 Total War and Factorio in my various repositories on github.
I guess it suffers from problem that FooMod class can easily become quite monstrous, but in the grand scheme of things I can live with it.
Multiple mods problem and obvious solutionSo here's the problem with the pattern above. Sometimes I want to play with multiple mods. This works just fine as long as they avoid modifying same files, but that's not always possible - often one file does so many things completely unrelated changes will end up editing it.
The only easy way around it is to merge both mods. This can't be reliably done by third party tools like diff3 as they have no idea about semantics of conflicting files, it needs to be done from within mod builder, something like this:
class FooBarMod < FooMod < BarMod def build_mod_files! FooMod::build_mod_files! BarMod::build_mod_files! end end FooBarMod.new("game/path").build!("build/path")
There's also a problem that there's no guarantee that these mods won't conflict in some way, as we're throwing all their methods and instance variables into same object.
module FooModMixin # tons of methods for different modifications and analyses end class FooMod < ModBuilder include FooModMixin def build_mod_files! # various commands to transform game file and create new ones end end
It also keeps the problem of possible conflicts between mod methods and instance variables.
Metaprogram multiple inheritance into ruby
FooMod.new("game/path").build!("build/foo") BarMod.new("game/path", "build/foo").build!("build/bar")
Then as long as we tell the game to load mods in correct order - bar overriding foo in case of conflicts - this will just work. Of course it's very easy to mess this up, and games are often unclear on how they resolve conflicts between mods - for example Europa Universalis 4 will load mods asciibetically so if you want to force some load order you need to put some number of spaces in front of your mod's name. Workable, but not too elegant.
Multistage solution followed by merging by file copy
FooMod.new("game/path").build!("build/foo") BarMod.new("game/path", "build/foo").build!("build/bar") system "cp -rf build/foo build/foo_and_bar" system "cp -rf build/bar build/foo_and_bar"
Delegation instead of inheritance
class FooMod def build_mod_files!(builder) @builder = builder # mod specific commands end end builder = ModBuilder.new("game/path") builder.apply!(FooMod.new) builder.apply!(BarMod.new) builder.save!("build/foo")
This has nice benefit of making it easy to split huge mods into multiple components, but suffers from @builder. littering the code. We could set @builder in constructor or in #build_mod_files! - either way we need to do that, as other methods of FooMod will do work by calling @builder's methods, and we definitely don't want to rewrite them all to take builder as extra argument.
Delegate then delegate backThis seems silly, but it isolates various modifications really well and avoids littering the code with @builder. everywhere.
class FooMod def build_mod_files!(builder) @builder = builder # mod specific commands end def method_missing(*args, &blk) @builder.send(*args, &blk) end end builder = ModBuilder.new("game/path") builder.apply!(FooMod.new) builder.apply!(BarMod.new) builder.save!("build/foo")
Separate classes for individual modifications and whole mods
class FooModification < GameModification def build_mod_files!(builder) # mod specific commands end end class FooMod < ModBuilder def build_mod_files! FooModification.new.build!(self) end end
class FooBarMod < ModBuilder def build_mod_files! FooModification.new.build!(self) BarModification.new.build!(self) end end
class FooModification < GameModification include UnitsAnalysis include ProvincesAnalysis def build_mod_files!(builder) # mod specific commands # can run any analyses from included mixins freely end end
Any good solution I missed?All these solutions have one or more of these downsides - poor isolation, extra boilerplate code, or added complexity.
The last one is probably most sensible, as boilerplate code is limited thanks to automated delegation, extra complexity can be hidden in base classes, and individual mods will generally be fairly straightforward and as isolated as you want them to be.
I guess I now need to rewrite all my mods to follow this pattern.