CleverRaven / Cataclysm-DDA

Cataclysm - Dark Days Ahead. A turn-based survival game set in a post-apocalyptic world.
http://cataclysmdda.org
Other
10.29k stars 4.12k forks source link

[Modding] Conditional object loading based on user's installed mods #48589

Closed jtgibson01 closed 1 year ago

jtgibson01 commented 3 years ago

Is your feature request related to a problem? Please describe.

Creating inter-compatible mods is limited by the fact that it is difficult to foresee in advance which other mods have been installed. While it is possible to create "glue" mods that override content of the original mod and/or one's own mod and which use both mods as a dependency, micromanaging these patches and keeping their content in separate locations is difficult both for the end user and for the modder. It creates clutter in the mod list and is not plug-and-play friendly for an end user -- q.v., the Undead People mod, which has numerous tileset submods that must all be vetted against the user's modlist and downloaded separately.

Describe the solution you'd like

I would like to have optional mod_whitelist and mod_blacklist JSON members in every factory-loadable object, as well as in parameters for entries in itemgroup distributions and collections (including nested ones for starting scenarios, etc.). When the whitelist is defined, the object is ignored if the player's world currently has no mod in their modlist with the matching mod id. The blacklist is of course the reverse.

For instance:

{"id": "jtveh_madmax_scenario"
,"type": "scenario"
,"mod_whitelist": ["jtmisc"]
    ,"name": "V8 Interceptor"
    ,"description": "You've got a powerful car at your side.  The Black-on-Black.  You have only one goal: to survive.  No friends.  Never build.  Never settle.  Never look back."
    ,"points": 1
    ,"start_name": "Gas station"
    ,"allowed_locs": [ "sloc_gas_station" ]
    ,"vehicle": "jtveh_interceptor"
    ,"professions": [ "jtmisc_highway_patrol_class" ]
    ,"flags": [ "LONE_START" ]
    }

The highway patrol class appears in one mod, while the V8 Interceptor vehicle is part of another mod. This scenario, when detected by the content loader, checks for the existence of the other mod before loading the content -- avoiding any errors about the missing profession if the user doesn't have the other mod installed.

I control both mods, so this isn't the best example, since I simply pulled the class out of my original mod and merged it into the other mod, but when the designer doesn't control the ecosystem of both mods, it would offer much better intercompatibility. Here's a more useful hypothetical example:

{"id": "city_pileup"
,"type": "vehicle_group"
,"mod_blacklist": ["Tankmod_Revived"]
    ,"vehicles": [
        ,["main_battle_tank", 400]
        ]
    }

If the other mod Tanks Mod Revived is found, this vehicle group will be ignored and the (current) mod's "main_battle_tank" won't be appended to the vehicle group. If Tanks Mod Revived is not found, this vehicle group will be loaded and the mod's own MBT will be loaded. The vehicle definition itself could also have a mod_blacklist, so as not to clutter the debug-spawning vehicle list with the vehicle. This also allows the designer more flexibility in choosing an object ID, rather than having to target the same object ID and hope that the user has arranged their installed mods in their mod list in an order that doesn't break the content by having the incorrect object with the same ID take precedence.

An even more useful example:

{"id": "rl_wizard"
,"type": "profession"
,"mod_whitelist": ["magiclysm"]
    ,"name": "Wizard"
    ,"description": "A wise student of ancient and forbidden knowledge; you are a wizard, a mystical practitioner of the arcane arts.  Transdimensional teleportion involuntarily brought you to this world being ravaged by the Deadite Scourge.  Most of the cosmos lacks the conditions for magic to exist, but it is clear that this world, too, flows with arcana, for how else could the Scourge invade?"
    ,"points": 0
    ,"skills": [
         {"name": "spellcraft", "level": 3}
        ,{"name": "chemistry", "level": 2}
        ,{"name": "cooking", "level": 2}
        ,{"name": "stabbing", "level": 1}
        ,{"name": "speech", "level": 1}
        ,{"name": "dodge", "level": 1}
        ]
    ,"items": {
        "both": {
            "items": [
                "loincloth_leather",
                "leathersandals",
                "backpack_leather",
                "cloak",
                "waterskin",
                "dandelion_wine",
                "pine_wine"
                ]
            ,"entries": [
                 {"item": "kris", "container-item": "sheath"}
                ,{"item": "dry_mushroom", "count": 8}
                ,{"group": "full_1st_aid"}
                ]
            }
        ,"female": ["bikini_top_leather"]
        }
    ,"traits": ["MANA_ADD2", "MAGUS", "STORMSHAPER", "FORCE_MAGE"]
    }

,{"id": "rl_wizard"
,"type": "profession"
,"mod_blacklist": ["magiclysm"]
    ,"name": "Wizard"
    ,"description": "A wise student of ancient and forbidden knowledge; you are a wizard, a mystical practitioner of the bionic arts.  Delving deep into military secrets, you learned of things that are best not said."
    ,"points": 0
    ,"skills": [
         {"name": "survival", "level": 3}
        ,{"name": "chemistry", "level": 2}
        ,{"name": "cooking", "level": 2}
        ,{"name": "stabbing", "level": 1}
        ,{"name": "speech", "level": 1}
        ,{"name": "dodge", "level": 1}
        ]
    ,"CBMs": [
        "bio_ethanol", "bio_flashlight", "bio_laser", "bio_chain_lightning", "bio_night",
        "bio_ads", "bio_teleport", "bio_cloak", "bio_power_storage_mkII"
        ]
    ,"items": {
        "both": {
            "items": [
                "loincloth_leather",
                "leathersandals",
                "backpack_leather",
                "cloak",
                "waterskin",
                "dandelion_wine",
                "pine_wine"
                ]
            ,"entries": [
                 {"item": "kris", "container-item": "sheath"}
                ,{"item": "dry_mushroom", "count": 8}
                ,{"group": "full_1st_aid"}
                ]
            }
        ,"female": ["bikini_top_leather"]
        }
    }

This particular mod would thereby have built-in support for Magiclysm in a single file. If Magiclysm were installed, the first prototype would be loaded, and the wizard would be a "true" medieval wizard, having magical spellcasting powers and etc. If Magiclysm is not installed, the second prototype would be loaded, and the wizard would instead be an eccentric user of compact bionic modules (like the character was in the original Roguelike Classes mod).

Other notes

Technically Undead People and any other tileset can simply merge everything into a single spritesheet and load that without regard to what mods the user has installed -- having a spritesheet key for an object that the user doesn't even have in the game breaks nothing and the only penalty is more memory usage. However, if conditional loading of tilesets based on this feature were also included, then it could save memory on the end user's machine by having specific graphics files only loaded based on whether the mods include whitelists. This would mean that a tileset could be reduced to a single mod that the user could install once and which would automatically expand to accommodate the mods in the user's load order, rather than either an all-or-nothing affair with a gigantic spritesheet or a boggle of a dozen spritesheets spread across as many submods.

ZhilkinSerg commented 3 years ago

Tilesets mods are already supported:

https://github.com/CleverRaven/Cataclysm-DDA/tree/master/data/mods/sees_player_retro

You can define mod as dependent on other mods:

https://github.com/CleverRaven/Cataclysm-DDA/blob/e7643a5faa3586750f31f053d849e25b36355f9e/data/mods/Graphical_Overmap_Magiclysm/modinfo.json#L10

You can define which tilesets tileset mod is compatible with:

https://github.com/CleverRaven/Cataclysm-DDA/blob/e7643a5faa3586750f31f053d849e25b36355f9e/data/mods/sees_player_retro/mod_tileset.json#L4

jtgibson01 commented 3 years ago

I was aware of all three of those features (I use all three in my mods). I get that it was a big post, but it was barely related to tilesets at all, and avoiding dependencies was explicitly mentioned as part of the intended purpose. It's an attempt to specify additional behaviour for the item/object factory that new objects or copy-from overrides should only be loaded if another mod is loaded (or not loaded), without having to add another mod as a dependency or maintain "patch soup" -- i.e., a dozen or more different patches to a mod's content based on the other mods in a user's load order, when a single mod could intelligently adapt itself to the mods that the user has in their load order... and telling the item factory/content loader to ignore an incoming object based on having a whitelist/blacklist should actually be relatively simple.

In short, it creates the possibility of a mod supporting other mods without requiring any/all of those mods installed.

The final example was of an updated version of 0.C's Roguelike Classes, with built-in support for Magiclysm, which would fall back to vanilla behaviour without Magiclysm. Currently, that kind of functionality can only be implemented with a submod that overrides the mod and has a dependency on Magiclysm, meaning that to get the cross-compatibility with Magiclysm, the user would need to download and activate two mods.

Tilesets mods are already supported:

Already discussed. I specifically pointed out how annoying the million-and-one UndeadPeople submod tilesets are. =)

You can define mod as dependent on other mods:

Also already discussed, though indirectly. I pointed out that supporting other mods requires the end user to download additional submods, creating additional burden on the end user, when conditional content loading would allow all of that burden to be shifted entirely onto the mod designer (if they wanted to) and the end user would only ever have to download a single encapsulated mod.

You can define which tilesets tileset mod is compatible with:

Actually not discussed! But not related to the feature request. =)

ZhilkinSerg commented 3 years ago

the user would need to download and activate two mods.

Yeah, so what's bad in that?

actual-nh commented 3 years ago

the user would need to download and activate two mods.

Yeah, so what's bad in that?

If it's just two mods, not a real problem. But for more complicated combinations?

ZhilkinSerg commented 3 years ago

the user would need to download and activate two mods.

Yeah, so what's bad in that?

If it's just two mods, not a real problem. But for more complicated combinations?

Complexity won't go anywhere if conditional object loading would be implemented.

actual-nh commented 3 years ago

the user would need to download and activate two mods.

Yeah, so what's bad in that?

If it's just two mods, not a real problem. But for more complicated combinations?

Complexity won't go anywhere if conditional object loading would be implemented.

It would be transferred from the end user to the programmer, true.

jtgibson01 commented 3 years ago

the user would need to download and activate two mods.

Yeah, so what's bad in that?

Adding on to what actual-nh said, it's also the degree of complexity of development relative to the actual benefit to the player. Supporting Magiclysm in that mod, for instance, is literally an entire submod, maintained in a separate location and offering another point of failure if the content is improperly maintained or if the player doesn't remember to download the updated submod or even simply forgets to activate it after downloading the main mod. All of that work... for a single starting profession.

A conditionally-loaded profession that recognises whether the player has Magiclysm or not, on the other hand, is easily maintained simultaneously by the developer and the player doesn't need to do a thing: if they download the updated version of the mod, they have everything ready to go.*

It would also collapse my unreleased/unannounced mod from three mods into one, and as the ur-example, collapse UndeadPeople into a neutron star.

* The only limitation is if the player would prefer the vanilla variant over the Magiclysm variant even if they have Magiclysm installed. That would in fact require a submod, to force loading of one or the other. I'd love to have an even more complex mod options/mod configuration system that specifies additional conditions for loading, but I'd rather not get ahead of myself. ;-)

Light-Wave commented 2 years ago

Complexity won't go anywhere if conditional object loading would be implemented.

It's my opinion that things would be far easier for the end user with this functionality, and probably slightly easier for the developer as well. Let's say you have a line of dialogue in an in-repo mod that you want to depend on whether Magiclysm, Dinomod, aftershock_exoplanet, and/or My_Sweet_Cataclysm is installed. That's potentially 8 different lines, and 8 different mods clogging up the in-repo mod list. All for one varying line of dialogue. And if you want to include 'glue' to more mods, that number grows exponentially.

If we instead have a mod blacklist/whitelist functionality, the developer would still need to write 8 different lines of dialogue. But instead of having 8 different mods the user have to pick between, there is only one that gets set up correctly automatically. For in-repo mods, it should be clear that 8 mods is unacceptable, while adding whitelist/blacklist info is not a big deal.

As for the functionality, I strongly support the idea. It would make adding tiny nods to other installed mods so much easier. I would recommend a way to specify if you need both mods, or if one of the listed ones are enough. Probably similar to how crafting recipes are made. You could probably also combine the blacklist and whilelist by using "not".

Something along the lines of

"mod_requirements": [ [ [ "mod_a" ] ], [ [ "mod_b" ] ], [ [ "mod_c" ], [ "mod_d" ] ], [ "not": { [ [ "mod_e" ] ] } ] ] Requires mod_a, mod_b, and either mod_c or mod_d to be present, as well as requiring mod_e to be absent.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. Please do not \'bump\' or comment on this issue unless you are actively working on it. Stale issues, and stale issues that are closed are still considered.

Night-Pryanik commented 1 year ago

Closing as stale, since stalebot can't do this by itself.