11#include " data.hpp"
22
33#include " hades_ida.hpp"
4+ #include " sjson_overlay.hpp"
45
56#include < hades2/pdb_symbol_map.hpp>
67#include < hooks/hooking.hpp>
@@ -21,6 +22,19 @@ namespace sgg
2122 };
2223} // namespace sgg
2324
25+ extern std::unordered_map<std::string, std::string> additional_map_files;
26+
27+ struct vo_file_registry
28+ {
29+ std::unordered_map<std::string, std::string> fsb_files;
30+ std::unordered_map<std::string, std::string> txt_files;
31+ };
32+ extern vo_file_registry additional_vo_files;
33+
34+ extern std::unordered_map<std::string, std::string> additional_bik_files;
35+ extern std::shared_mutex g_plugin_files_mutex;
36+ extern int ends_with (const char * str, const char * suffix);
37+
2438// Defined in main.cpp — file redirect maps for custom GPK/PKG assets
2539extern std::unordered_map<std::string, std::string> additional_granny_files;
2640extern std::unordered_map<std::string, std::string> additional_package_files;
@@ -267,7 +281,29 @@ namespace lua::hades::data
267281 {
268282 std::scoped_lock l (big::lua_manager_extension::g_manager_mutex);
269283
270- g_sjson_FileStream_to_filepath[g_current_file_stream] = output_;
284+ // If the output was redirected to an SJSON overlay path, it won't match sjson.hook filters
285+ // sjson.hook filters use Content/ paths, which we need to reconstruct
286+ // The base path is the ResourceDirectory root (e.g. "C:\...\Content\Game\Animations"), and pathComponent is the filename
287+ // We check if the output contains the overlay marker and fall back to the original path
288+ std::string output_str = (char *)output_.u8string ().c_str ();
289+ if (output_str.find (sjson_overlay::SJSON_DATA_DIR_NAME ) != std::string::npos)
290+ {
291+ // Output was redirected to overlay - build the original Content/ path instead
292+ char original_path[512 ];
293+ strncpy (original_path, basePath, sizeof (original_path) - 1 );
294+ original_path[sizeof (original_path) - 1 ] = ' \0 ' ;
295+ size_t base_len = strlen (original_path);
296+ if (base_len > 0 && original_path[base_len - 1 ] != ' \\ ' && original_path[base_len - 1 ] != ' /' )
297+ {
298+ strncat (original_path, " \\ " , sizeof (original_path) - base_len - 1 );
299+ }
300+ strncat (original_path, pathComponent, sizeof (original_path) - strlen (original_path) - 1 );
301+ g_sjson_FileStream_to_filepath[g_current_file_stream] = original_path;
302+ }
303+ else
304+ {
305+ g_sjson_FileStream_to_filepath[g_current_file_stream] = output_;
306+ }
271307 }
272308 }
273309 }
@@ -487,6 +523,121 @@ namespace lua::hades::data
487523 ns.set_function (" add_granny_file" , add_granny_file);
488524 ns.set_function (" add_package_file" , add_package_file);
489525
526+ // Lua API: Field
527+ // Table: data
528+ // Name: SJSON_DATA_DIR_NAME
529+ // Value: "Hell2Modding-SJSON"
530+ // The canonical directory name for the SJSON data overlay.
531+ // Mods must place .sjson files in plugins_data/<mod-guid>/<SJSON_DATA_DIR_NAME>/Animations/, Text/{lang}/, etc.
532+ // Hell2Modding scans this directory at startup and injects discovered .sjson files into the engine's loading pipeline.
533+ ns[" SJSON_DATA_DIR_NAME" ] = sjson_overlay::SJSON_DATA_DIR_NAME ;
534+
535+ // Lua API: Function
536+ // Table: data
537+ // Name: register_sjson_file
538+ // Param: absolute_path: string: The absolute filesystem path to a .sjson file inside a <SJSON_DATA_DIR_NAME> directory.
539+ // Returns: boolean: true if registered successfully, false if the file is a duplicate, not a .sjson, or the path does not contain <SJSON_DATA_DIR_NAME>.
540+ // Registers a .sjson file so the engine discovers and loads it as if it were in the game's `Content/Game/` directory.
541+ // The engine-relative path is inferred automatically: files inside `plugins_data/<mod>/<SJSON_DATA_DIR_NAME>/` map to `Content/Game/`.
542+ // For example, `plugins_data/<mod-guid>/<SJSON_DATA_DIR_NAME>/Animations/Foo.sjson` is loaded as `Content/Game/Animations/Foo.sjson`.
543+ // At startup, Hell2Modding automatically scans every mod's <SJSON_DATA_DIR_NAME> directory and registers any .sjson files found.
544+ // Use this function to dynamically register files created during the current session (e.g. a first-time install placing a file into plugins_data).
545+ ns.set_function (" register_sjson_file" , [](const std::string& absolute_path) -> bool {
546+ std::string normalized = sjson_overlay::normalize_path (absolute_path);
547+ const std::string marker = std::string (sjson_overlay::SJSON_DATA_DIR_NAME ) + " /" ;
548+ auto pos = normalized.find (marker);
549+ if (pos == std::string::npos)
550+ {
551+ LOG (WARNING ) << " [SJSON] register_sjson_file: aborting, path does not contain '" << sjson_overlay::SJSON_DATA_DIR_NAME << " /' directory: " << absolute_path;
552+ return false ;
553+ }
554+ // Convention: <SJSON_DATA_DIR_NAME> implicitly maps to Game/ in Content
555+ std::string logical_relpath = " Game/" + normalized.substr (pos + marker.size ());
556+ return sjson_overlay::register_content_file (logical_relpath, absolute_path);
557+ });
558+
559+ // Lua API: Function
560+ // Table: data
561+ // Name: register_content_directory
562+ // Param: absolute_base_path: string: Absolute path to a directory whose structure mirrors `Content/Game/` (e.g. containing `Animations/`, `Text/en/`, etc.)
563+ // Scans the directory recursively and registers all .sjson files found. Each file's engine path is derived from its position in the directory tree.
564+ // This is the same scan that Hell2Modding performs automatically at startup for `plugins_data/*/<SJSON_DATA_DIR_NAME>/`.
565+ ns.set_function (" register_content_directory" , [](const std::string& absolute_base_path) {
566+ sjson_overlay::scan_content_directory (std::filesystem::path (absolute_base_path));
567+ });
568+
569+ // Lua API: Function
570+ // Table: data
571+ // Name: register_file_redirect
572+ // Param: content_relative_path: string: The path relative to Content/, e.g. "Maps/D_Hub.map_text" or "Maps/bin/D_Hub.thing_bin"
573+ // Param: absolute_path: string: The absolute filesystem path to the actual file
574+ // Returns: boolean: true if registered, false if duplicate
575+ // Registers a file redirect so the engine loads it from an external location instead of Content/.
576+ // Unlike register_content_file (SJSON-only), this works for any file type that the engine loads via fsAppendPathComponent (maps, etc.).
577+ // No directory convention is enforced - the caller provides both paths.
578+ ns.set_function (" register_file_redirect" , [](const std::string& content_relative_path, const std::string& absolute_path) -> bool {
579+ std::string normalized = sjson_overlay::normalize_path (content_relative_path);
580+
581+ std::unique_lock lock (sjson_overlay::g_overlay_mutex);
582+ if (sjson_overlay::g_path_index.count (normalized))
583+ {
584+ return false ;
585+ }
586+ sjson_overlay::g_path_index[normalized] = absolute_path;
587+ LOG (INFO ) << " Adding file redirect: " << normalized << " -> " << absolute_path;
588+ return true ;
589+ });
590+
591+ // Lua API: Function
592+ // Table: data
593+ // Name: register_plugin_file
594+ // Param: filename: string: The filename (e.g. "D_Boss01.map_text", "HadesBattleIdle.bik", "Zagreus.fsb")
595+ // Param: absolute_path: string: The absolute filesystem path to the file
596+ // Returns: boolean: true if registered, false if already registered or unsupported extension
597+ // Registers a file for engine injection and redirect. Routes to the appropriate internal registry.
598+ // Supported extensions: .map_text, .thing_bin, .bik, .bik_atlas, .fsb, .txt.
599+ // SJSON files should use `register_sjson_file` instead.
600+ ns.set_function (" register_plugin_file" , [](const std::string& filename, const std::string& absolute_path) -> bool {
601+ std::unordered_map<std::string, std::string>* target = nullptr ;
602+ const char * label = nullptr ;
603+
604+ if (ends_with (filename.c_str (), " .map_text" ) || ends_with (filename.c_str (), " .thing_bin" ))
605+ {
606+ target = &additional_map_files;
607+ label = " map" ;
608+ }
609+ else if (ends_with (filename.c_str (), " .bik" ) || ends_with (filename.c_str (), " .bik_atlas" ))
610+ {
611+ target = &additional_bik_files;
612+ label = " bik" ;
613+ }
614+ else if (ends_with (filename.c_str (), " .fsb" ))
615+ {
616+ target = &additional_vo_files.fsb_files ;
617+ label = " VO (fsb)" ;
618+ }
619+ else if (ends_with (filename.c_str (), " .txt" ))
620+ {
621+ target = &additional_vo_files.txt_files ;
622+ label = " VO (txt)" ;
623+ }
624+
625+ if (!target)
626+ {
627+ LOG (WARNING ) << " register_plugin_file: unsupported extension for '" << filename << " '" ;
628+ return false ;
629+ }
630+
631+ std::unique_lock lock (g_plugin_files_mutex);
632+ if (target->count (filename))
633+ {
634+ return false ;
635+ }
636+ (*target)[filename] = absolute_path;
637+ LOG (INFO ) << " Adding to " << label << " files: " << absolute_path;
638+ return true ;
639+ });
640+
490641 state[" sol.__h2m_LoadPackages__" ] = state[" LoadPackages" ];
491642 // Lua API: Function
492643 // Table: game
0 commit comments