Skip to content

Commit

Permalink
Mod Compatibility: Only load files within "mod_interactions" if requi…
Browse files Browse the repository at this point in the history
…site mod is loaded (#76632)

* Add feature programatically

* adjust existing folders to meet new styling

* move interaction files to load after main mod loading

* Force mod_interaction files to load after normal mod files

* Refactor code to remove disgusting tuple logic

* Add astyling and comments

* Delete data/mods/Aftershock/mod_interactions/Defense_Mode directory

uncapitalizing directories is difficult

* Delete data/mods/Defense_Mode/mod_interactions/Aftershock directory

* Delete data/mods/Defense_Mode/mod_interactions/Magiclysm directory

* Delete data/mods/Defense_Mode/mod_interactions/Megafauna directory

* Delete data/mods/Defense_Mode/mod_interactions/MindOverMatter directory

* Delete data/mods/Defense_Mode/mod_interactions/Xedra_Evolved directory

* Delete data/mods/Defense_Mode/mod_interactions/My_Sweet_Cataclysm directory

* Delete data/mods/Magiclysm/mod_interactions/Defense_Mode directory

* Delete data/mods/Megafauna/mod_interactions/Defense_Mode directory

* Delete data/mods/Xedra_Evolved/mod_interactions/Defense_Mode directory

* restart tests phase 1

* restart tests phase 2

* make for loop var const

* extra fix for const for loop

* add doc

* Update doc/MOD_COMPATABILITY.md

---------

Co-authored-by: Maleclypse <[email protected]>
  • Loading branch information
b3brodie and Maleclypse authored Oct 8, 2024
1 parent 00c9ead commit d30e8f3
Show file tree
Hide file tree
Showing 65 changed files with 249 additions and 11 deletions.
33 changes: 33 additions & 0 deletions doc/MOD_COMPATABILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Mod Compatability

## Summary

Mods are capable of dynamically loading directories based on if other mods are loaded in the world. This will prevent file contents within the mod_interactions folder from being read unless they are part of a folder named after a loaded mod's id.

## Guide

In order to dynamically load mod content, files must be placed within subdirectories named after other mod ids (capitalization is checked) within the mod_interactions folder.

Example:
Mod 1: Mind Over Matter (id:mindovermatter)
Mod 2: Xedra Evolved (id:xedra_evolved)

If Xedra wishes to load a file only when Mind Over Matter is active: mom_compat_data.json, it must be located as such:
Xedra_Evolved/mod_interactions/mindovermatter/mom_compat_data.json

Files located within the mod_interactions folders are always loaded after other mod content for every mod is loaded.

## Limitations

Currently, this functionality only loads / unloads files based on if other mods are active for the particular world. It does not suppress any other warnings beyond this function.

In particular, when designing mod compatability content an author will likely want to redefine certain ids to have new definitions, flags, etc. If attempting to do this within the same overall mod folder, this will throw a duplicate definition error. To get around this, instead of a mod redefining its own content within its own mod_interactions folder, the author should redefine its own content using the other mods mod_interaction folder.

Example:
Mod 1: Mind Over Matter (id:mindovermatter)
Mod 2: Xedra Evolved (id:xedra_evolved)

If xedra wants an item to have extra damage while Mind Over Matter is loaded, the author should place the new definition in the following:
MindOverMatter/mod_interactions/xedra_evolved/xedra_compat_data.json

TODO: remove this limitation entirely by adjusting the check to take into account the mod_interaction id source as well
82 changes: 73 additions & 9 deletions src/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,15 @@ bool is_directory( const fs::directory_entry &entry )
// If at_end is true, returns whether entry's name ends in match.
// Otherwise, returns whether entry's name contains match.
//--------------------------------------------------------------------------------------------------
bool name_contains( const fs::directory_entry &entry, const std::string &match, const bool at_end )
{
std::string entry_name = entry.path().filename().u8string();
bool name_contains( const fs::directory_entry &entry, const std::string &match, const bool at_end,
const bool match_path )
{
std::string entry_name;
if( match_path ) {
entry_name = entry.path().u8string();
} else {
entry_name = entry.path().filename().u8string();
}
const size_t len_fname = entry_name.length();
const size_t len_match = match.length();

Expand Down Expand Up @@ -421,15 +427,36 @@ std::vector<std::string> get_files_from_path( const std::string &pattern,
{
return find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, match_extension );
return name_contains( entry, pattern, match_extension, false );
} );
}
std::vector<cata_path> get_files_from_path( const std::string &pattern,
const cata_path &root_path, const bool recursive_search, const bool match_extension )
{
return find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, match_extension );
return name_contains( entry, pattern, match_extension, false );
} );
}

std::vector<std::string> get_files_from_path_with_path_exclusion( const std::string &pattern,
const std::string &pattern_clash,
const std::string &root_path, const bool recursive_search, const bool match_extension )
{
return find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, match_extension, false ) &&
!name_contains( entry, pattern_clash, false, true );
} );
}
std::vector<cata_path> get_files_from_path_with_path_exclusion( const std::string &pattern,
const std::string &pattern_clash,
const cata_path &root_path, const bool recursive_search, const bool match_extension )
{
return find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, match_extension, false ) &&
!name_contains( entry, pattern_clash, false, true );
} );
}

Expand All @@ -449,7 +476,7 @@ std::vector<std::string> get_directories_with( const std::string &pattern,

auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, true );
return name_contains( entry, pattern, true, false );
} );

// Chop off the file names. Dir path MUST be splitted by '/'
Expand All @@ -471,7 +498,7 @@ std::vector<cata_path> get_directories_with( const std::string &pattern,

auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return name_contains( entry, pattern, true );
return name_contains( entry, pattern, true, false );
} );

// Chop off the file names. Dir path MUST be splitted by '/'
Expand Down Expand Up @@ -504,7 +531,7 @@ std::vector<std::string> get_directories_with( const std::vector<std::string> &p
auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return std::any_of( ext_beg, ext_end, [&]( const std::string & ext ) {
return name_contains( entry, ext, true );
return name_contains( entry, ext, true, false );
} );
} );

Expand Down Expand Up @@ -532,7 +559,7 @@ std::vector<cata_path> get_directories_with( const std::vector<std::string> &pat
auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return std::any_of( ext_beg, ext_end, [&]( const std::string & ext ) {
return name_contains( entry, ext, true );
return name_contains( entry, ext, true, false );
} );
} );

Expand All @@ -547,6 +574,43 @@ std::vector<cata_path> get_directories_with( const std::vector<std::string> &pat
return files;
}

/**
* Finds all directories within given path
* @param root_path Search root.
* @param recursive_search Be recurse or not.
* @return vector or directories without pattern filename at end.
*/
std::vector<std::string> get_directories( const std::string &root_path,
const bool recursive_search )
{
auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return dir_exist( entry.path() );
} );

files.erase( std::unique( std::begin( files ), std::end( files ) ), std::end( files ) );

return files;
}

/**
* Finds all directories within given path
* @param root_path Search root.
* @param recursive_search Be recurse or not.
* @return vector or directories without pattern filename at end.
*/
std::vector<cata_path> get_directories( const cata_path &root_path, const bool recursive_search )
{
auto files = find_file_if_bfs( root_path, recursive_search, [&]( const fs::directory_entry & entry,
bool ) {
return dir_exist( entry.path() );
} );

files.erase( std::unique( std::begin( files ), std::end( files ) ), std::end( files ) );

return files;
}

bool copy_file( const std::string &source_path, const std::string &dest_path )
{
std::ifstream source_stream( fs::u8path( source_path ),
Expand Down
27 changes: 27 additions & 0 deletions src/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ std::vector<std::string> get_files_from_path( const std::string &pattern,
std::vector<cata_path> get_files_from_path( const std::string &pattern,
const cata_path &root_path, bool recursive_search = false,
bool match_extension = false );
/**
* Returns a vector of files or directories matching pattern at @p root_path excluding those who's path matches @p pattern_clash.
*
* Searches through the directory tree breadth-first. Directories are searched in lexical
* order. Matching files within in each directory are also ordered lexically.
*
* @param pattern The sub-string to match.
* @param pattern_clash The sub-string to exclude files whose paths match.
* @param root_path The path relative to the current working directory to search; empty means ".".
* @param recursive_search Whether to recursively search sub directories.
* @param match_extension If true, match pattern at the end of file names. Otherwise, match anywhere
* in the file name.
*/
std::vector<std::string> get_files_from_path_with_path_exclusion( const std::string &pattern,
const std::string &pattern_clash,
const std::string &root_path = "", bool recursive_search = false,
bool match_extension = false );
std::vector<cata_path> get_files_from_path_with_path_exclusion( const std::string &pattern,
const std::string &pattern_clash,
const cata_path &root_path, bool recursive_search = false,
bool match_extension = false );

//--------------------------------------------------------------------------------------------------
/**
Expand All @@ -92,6 +113,12 @@ std::vector<cata_path> get_directories_with( const std::vector<std::string> &pat
std::vector<cata_path> get_directories_with( const std::string &pattern,
const cata_path &root_path = {}, bool recursive_search = false );

std::vector<std::string> get_directories( const std::string &root_path = "",
bool recursive_search = false );

std::vector<cata_path> get_directories( const cata_path &root_path = {}, bool recursive_search =
false );

bool copy_file( const std::string &source_path, const std::string &dest_path );
bool copy_file( const cata_path &source_path, const cata_path &dest_path );

Expand Down
22 changes: 20 additions & 2 deletions src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,15 @@ void game::load_data_from_dir( const cata_path &path, const std::string &src )
DynamicDataLoader::get_instance().load_data_from_path( path, src );
}

void game::load_mod_data_from_dir( const cata_path &path, const std::string &src )
{
DynamicDataLoader::get_instance().load_mod_data_from_path( path, src );
}
void game::load_mod_interaction_data_from_dir( const cata_path &path, const std::string &src )
{
DynamicDataLoader::get_instance().load_mod_interaction_files_from_path( path, src );
}

#if defined(TUI)
// in ncurses_def.cpp
extern void check_encoding(); // NOLINT
Expand Down Expand Up @@ -3235,7 +3244,9 @@ void game::load_world_modfiles()
load_packs( _( "Loading files" ), mods );

// Load additional mods from that world-specific folder
load_data_from_dir( PATH_INFO::world_base_save_path_path() / "mods", "custom" );
load_mod_data_from_dir( PATH_INFO::world_base_save_path_path() / "mods", "custom" );
load_mod_interaction_data_from_dir( PATH_INFO::world_base_save_path_path() / "mods" /
"mod_interactions", "custom" );

DynamicDataLoader::get_instance().finalize_loaded_data();
}
Expand All @@ -3260,9 +3271,16 @@ void game::load_packs( const std::string &msg, const std::vector<mod_id> &packs
if( mod.ident.str() == "test_data" ) {
check_plural = check_plural_t::none;
}
load_data_from_dir( mod.path, mod.ident.str() );
load_mod_data_from_dir( mod.path, mod.ident.str() );
}

for( const auto &e : available ) {
loading_ui::show( msg, e->name() );
const MOD_INFORMATION &mod = *e;
load_mod_interaction_data_from_dir( mod.path / "mod_interactions", mod.ident.str() );
}


std::unordered_set<mod_id> removed_mods {
MOD_INFORMATION_Graphical_Overmap // Removed in 0.I
};
Expand Down
4 changes: 4 additions & 0 deletions src/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ class game
protected:
/** Loads dynamic data from the given directory. May throw. */
void load_data_from_dir( const cata_path &path, const std::string &src );
/** Loads dynamic data from the given directory. Excludes files from 'mod_interactions' sub-directory. May throw. */
void load_mod_data_from_dir( const cata_path &path, const std::string &src );
/** Loads dynamic data from the folder if it is part of a subdirectory that is named after a currently loaded mod_id. May throw. */
void load_mod_interaction_data_from_dir( const cata_path &path, const std::string &src );
public:
void setup();
/** Saving and loading functions. */
Expand Down
73 changes: 73 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,79 @@ void DynamicDataLoader::load_data_from_path( const cata_path &path, const std::s
}
}

void DynamicDataLoader::load_mod_data_from_path( const cata_path &path, const std::string &src )
{
cata_assert( !finalized &&
"Can't load additional data after finalization. Must be unloaded first." );
// We assume that each folder is consistent in itself,
// and all the previously loaded folders.
// E.g. the core might provide a vpart "frame-x"
// the first loaded mode might provide a vehicle that uses that frame
// But not the other way round.

std::vector<cata_path> files;
// if give path is a directory
if( dir_exist( path.get_unrelative_path() ) ) {
const std::vector<cata_path> dir_files = get_files_from_path_with_path_exclusion( ".json",
"mod_interactions", path, true, false );
files.insert( files.end(), dir_files.begin(), dir_files.end() );
// if given path is an individual file
} else if( file_exist( path.get_unrelative_path() ) ) {
files.emplace_back( path );
}

// iterate over each file
for( const cata_path &file : files ) {
try {
// parse it
JsonValue jsin = json_loader::from_path( file );
load_all_from_json( jsin, src, path, file );
} catch( const JsonError &err ) {
throw std::runtime_error( err.what() );
}
}
}

void DynamicDataLoader::load_mod_interaction_files_from_path( const cata_path &path,
const std::string &src )
{
cata_assert( !finalized &&
"Can't load additional data after finalization. Must be unloaded first." );

std::vector<mod_id> &loaded_mods = world_generator->active_world->active_mod_order;
std::vector<cata_path> files;

if( dir_exist( path.get_unrelative_path() ) ) {

// obtain folders within mod_interactions to see if they match loaded mod ids
const std::vector<cata_path> interaction_folders = get_directories( path, false );

for( const cata_path &f : interaction_folders ) {
bool is_mod_loaded = false;
for( mod_id id : loaded_mods ) {
if( id.str() == f.get_unrelative_path().filename().string() ) {
is_mod_loaded = true;
}
}
if( is_mod_loaded ) {
const std::vector<cata_path> interaction_files = get_files_from_path( ".json", f, true, true );
files.insert( files.end(), interaction_files.begin(), interaction_files.end() );
}
}
}

// iterate over each file
for( const cata_path &file : files ) {
try {
// parse it
JsonValue jsin = json_loader::from_path( file );
load_all_from_json( jsin, src, path, file );
} catch( const JsonError &err ) {
throw std::runtime_error( err.what() );
}
}
}

void DynamicDataLoader::load_all_from_json( const JsonValue &jsin, const std::string &src,
const cata_path &base_path, const cata_path &full_path )
{
Expand Down
19 changes: 19 additions & 0 deletions src/init.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ class DynamicDataLoader
*/
/*@{*/
void load_data_from_path( const cata_path &path, const std::string &src );
/**
* Load all data from json files located in
* the path (recursive) except for those within the mod_interactions folder.
* @param path Either a folder (recursively load all
* files with the extension .json), or a file (load only
* that file, don't check extension).
* @param src String identifier for mod this data comes from.
* @throws std::exception on all kind of errors.
*/
/*@{*/
void load_mod_data_from_path( const cata_path &path, const std::string &src );
/**
* Load directories located within the given path if they are named after a currently loaded mod id.
* @param path a folder.
* @param src String identifier for mod this data comes from.
* @throws std::exception on all kind of errors.
*/
/*@{*/
void load_mod_interaction_files_from_path( const cata_path &path, const std::string &src );
/*@}*/
/**
* Deletes and unloads all the data previously loaded with
Expand Down

0 comments on commit d30e8f3

Please sign in to comment.