Added enforcing language with EnderalLanguage.ini
This commit is contained in:
parent
8b7b1084c0
commit
274ed7983f
@ -16,6 +16,7 @@
|
||||
#include "Patches/PluginsTxtPatch.h"
|
||||
#include "Patches/FormTypeCollisionDetector.h"
|
||||
#include "Patches/GogVramLeakFix.h"
|
||||
#include "Patches/ForceLanguage.h"
|
||||
|
||||
using namespace SKSE;
|
||||
|
||||
@ -214,6 +215,8 @@ SKSEPluginLoad(const LoadInterface* skse) {
|
||||
|
||||
RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr;
|
||||
|
||||
ForceLanguage::Install("Data/SKSE/Plugins/EnderalLanguage.ini");
|
||||
|
||||
EventListener::Install();
|
||||
|
||||
GetPapyrusInterface()->Register(Papyrus::Bind);
|
||||
|
||||
115
source/Enderal DLL/src/Patches/ForceLanguage.h
Normal file
115
source/Enderal DLL/src/Patches/ForceLanguage.h
Normal file
@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <SimpleIni.h>
|
||||
|
||||
// Reads the language from Data/SKSE/Plugins/EnderalLanguage.ini and enforces it at runtime,
|
||||
// independently of sLanguage in Skyrim.ini.
|
||||
//
|
||||
// The engine keeps the active language in the string setting sLanguage:General and reads that
|
||||
// Setting's data.s directly (the object is statically allocated, so the reads are hardcoded
|
||||
// addresses) whenever it builds a localized path:
|
||||
//
|
||||
// Strings\<plugin>_<language>.STRINGS / .DLSTRINGS / .ILSTRINGS
|
||||
// Interface\Translate_<language>.txt
|
||||
// Interface\Controls\<device>\<language>.txt
|
||||
// credits_<language>.txt
|
||||
//
|
||||
// Overwriting data.s therefore switches the language everywhere at once, but only inside a narrow
|
||||
// window: INISettingCollection::ReadSetting puts the Skyrim.ini value back (that file does contain
|
||||
// sLanguage, unlike sIntroSequence), and TESDataHandler has already loaded the .STRINGS files by
|
||||
// the time SKSE dispatches kDataLoaded. Hooking ReadSetting sidesteps both problems - the value is
|
||||
// re-applied right after every read of that setting, whenever the engine reads the INI.
|
||||
class ForceLanguage final : public RE::INISettingCollection
|
||||
{
|
||||
public:
|
||||
static void Install(const char* a_iniPath)
|
||||
{
|
||||
if (!ReadINI(a_iniPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto collection = RE::INISettingCollection::GetSingleton();
|
||||
setting = collection ? collection->GetSetting("sLanguage:General") : nullptr;
|
||||
|
||||
if (!setting) {
|
||||
logger::error("sLanguage:General is missing, cannot enforce the language");
|
||||
return;
|
||||
}
|
||||
|
||||
REL::Relocation<uintptr_t> vtbl(RE::INISettingCollection::VTABLE[0]);
|
||||
_ReadSetting = vtbl.write_vfunc(0x4, &ReadSettingEx);
|
||||
|
||||
Apply();
|
||||
|
||||
logger::info("Enforcing language {}", requested);
|
||||
}
|
||||
|
||||
private:
|
||||
bool ReadSettingEx(RE::Setting* a_setting)
|
||||
{
|
||||
const auto result = _ReadSetting(this, a_setting);
|
||||
|
||||
if (a_setting == setting) {
|
||||
Apply();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// The engine owns data.s once the setting is managed and may overwrite it in place with a value
|
||||
// of equal or smaller length, so hand out a private buffer and restamp it from requested.
|
||||
static void Apply()
|
||||
{
|
||||
strcpy_s(buffer, sizeof(buffer), requested.c_str());
|
||||
setting->data.s = buffer;
|
||||
}
|
||||
|
||||
static bool ReadINI(const char* a_iniPath)
|
||||
{
|
||||
if (!std::filesystem::exists(a_iniPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CSimpleIniA ini;
|
||||
ini.SetUnicode(false);
|
||||
ini.SetMultiKey(false);
|
||||
|
||||
if (const auto result = ini.LoadFile(a_iniPath); result != SI_OK) {
|
||||
logger::error("Failed to read {} (error {})", a_iniPath, static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
// [General] mirrors Skyrim.ini, a section-less key is accepted as well.
|
||||
requested = ini.GetValue("General", "sLanguage", ini.GetValue("", "sLanguage", ""));
|
||||
|
||||
constexpr auto whitespace = " \t\r\n"sv;
|
||||
const auto first = requested.find_first_not_of(whitespace);
|
||||
requested = first == std::string::npos
|
||||
? std::string()
|
||||
: requested.substr(first, requested.find_last_not_of(whitespace) - first + 1);
|
||||
|
||||
if (requested.empty()) {
|
||||
logger::warn("{} has no sLanguage value, keeping the language from Skyrim.ini", a_iniPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requested.size() >= sizeof(buffer)) {
|
||||
logger::error("sLanguage value \"{}\" in {} is too long", requested, a_iniPath);
|
||||
requested.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enderal ships its strings loose, so a missing file means the language is most likely misspelled.
|
||||
const auto strings = std::format("Data/Strings/enderal - forgotten stories_{}.strings", requested);
|
||||
if (!std::filesystem::exists(strings)) {
|
||||
logger::warn("{} does not exist, {} may not be a supported language", strings, requested);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline static std::string requested; // as configured, never handed to the engine
|
||||
inline static char buffer[64]{}; // what data.s points at, may be overwritten in place
|
||||
inline static RE::Setting* setting{ nullptr };
|
||||
inline static REL::Relocation<decltype(&ReadSettingEx)> _ReadSetting;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user