Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mod Compatibility: Only load files within "mod_interactions" if requisite mod is loaded #76632

Merged
merged 22 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8ae2857
Add feature programatically
b3brodie Sep 23, 2024
cc1c681
adjust existing folders to meet new styling
b3brodie Sep 23, 2024
12c4745
move interaction files to load after main mod loading
b3brodie Sep 23, 2024
98a2d70
Force mod_interaction files to load after normal mod files
b3brodie Sep 24, 2024
a9f17b3
Refactor code to remove disgusting tuple logic
b3brodie Sep 25, 2024
922c390
Add astyling and comments
b3brodie Sep 25, 2024
88375ef
Delete data/mods/Aftershock/mod_interactions/Defense_Mode directory
b3brodie Sep 27, 2024
8644ada
Delete data/mods/Defense_Mode/mod_interactions/Aftershock directory
b3brodie Sep 27, 2024
68b1d85
Delete data/mods/Defense_Mode/mod_interactions/Magiclysm directory
b3brodie Sep 27, 2024
4e2db3d
Delete data/mods/Defense_Mode/mod_interactions/Megafauna directory
b3brodie Sep 27, 2024
ccfeb0b
Delete data/mods/Defense_Mode/mod_interactions/MindOverMatter directory
b3brodie Sep 27, 2024
8fe0d30
Delete data/mods/Defense_Mode/mod_interactions/Xedra_Evolved directory
b3brodie Sep 27, 2024
bbac887
Delete data/mods/Defense_Mode/mod_interactions/My_Sweet_Cataclysm dir…
b3brodie Sep 27, 2024
520eb3d
Delete data/mods/Magiclysm/mod_interactions/Defense_Mode directory
b3brodie Sep 27, 2024
1f3bd68
Delete data/mods/Megafauna/mod_interactions/Defense_Mode directory
b3brodie Sep 27, 2024
fe62334
Delete data/mods/Xedra_Evolved/mod_interactions/Defense_Mode directory
b3brodie Sep 27, 2024
4103680
restart tests phase 1
b3brodie Sep 27, 2024
8a48208
restart tests phase 2
b3brodie Sep 27, 2024
8989f04
make for loop var const
b3brodie Sep 29, 2024
bbfb780
extra fix for const for loop
b3brodie Sep 29, 2024
c3bf498
add doc
b3brodie Oct 5, 2024
0eb4113
Update doc/MOD_COMPATABILITY.md
Maleclypse Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -606,6 +606,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 @@ -3233,7 +3242,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 @@ -3258,9 +3269,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
Loading