Skip to content

Commit bdfec08

Browse files
NikkelMxiaoxiao921
andauthored
feat: discover and inject various file types from plugins_data (#30)
* feat(sjson): discover and inject sjson files from plugins_data * Normalize paths * Fix late registration lookup * Added warning if two mods register the same file * Fixed string normalization breaking injection * feat(maps): discover and inject .thing_bin and .map_text files from plugins_data * Removed unnecessary guard * Auto-inject VO .fsb and .txt files * Auto-inject bink .bik and .bik_atlas files, common register_plugin_file lua function * Applied review comments * set_sjson_data_path lua doc * Updated strstr to ends_with * Use strcmp == 0 * Removed noisy bink log * Also use strcmp == 0 for gpk files --------- Co-authored-by: Quentin <837334+xiaoxiao921@users.noreply.github.com>
1 parent 2ea0085 commit bdfec08

5 files changed

Lines changed: 896 additions & 36 deletions

File tree

src/lua_extensions/bindings/hades/data.cpp

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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
2539
extern std::unordered_map<std::string, std::string> additional_granny_files;
2640
extern 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

src/lua_extensions/lua_module_ext.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#pragma once
22

33
#include "bindings/hades/inputs.hpp"
4+
#include "sjson_overlay.hpp"
45
#include "lua/lua_module.hpp"
56

7+
#include <paths/paths.hpp>
8+
69
namespace big
710
{
811
struct lua_module_data_ext
@@ -32,11 +35,13 @@ namespace big
3235
lua_module_ext(const module_info& module_info, sol::environment& env) :
3336
lua_module(module_info, env)
3437
{
38+
set_sjson_data_path();
3539
}
3640

3741
lua_module_ext(const module_info& module_info, sol::state_view& state) :
3842
lua_module(module_info, state)
3943
{
44+
set_sjson_data_path();
4045
}
4146

4247
inline void cleanup() override
@@ -45,5 +50,23 @@ namespace big
4550

4651
m_data_ext = {};
4752
}
53+
54+
private:
55+
void set_sjson_data_path()
56+
{
57+
sol::table ns = m_env["_PLUGIN"];
58+
if (ns.valid())
59+
{
60+
auto sjson_data_path = g_file_manager.get_project_folder("plugins_data").get_path() / m_info.m_guid / sjson_overlay::SJSON_DATA_DIR_NAME;
61+
auto sjson_data_path_string = std::string(reinterpret_cast<const char*>(sjson_data_path.u8string().c_str()));
62+
63+
// Lua API: Field
64+
// Table: _ENV - Plugin Specific Global Table
65+
// Field: _PLUGIN.sjson_data_path: string
66+
// The absolute path to the plugin's SJSON data directory (plugins_data/<mod-guid>/Hell2Modding-SJSON/).
67+
// Mod authors can use this to discover or create .sjson files at runtime.
68+
ns["sjson_data_path"] = sjson_data_path_string;
69+
}
70+
}
4871
};
4972
} // namespace big

0 commit comments

Comments
 (0)