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/PluginsTxtPatch.h"
|
||||||
#include "Patches/FormTypeCollisionDetector.h"
|
#include "Patches/FormTypeCollisionDetector.h"
|
||||||
#include "Patches/GogVramLeakFix.h"
|
#include "Patches/GogVramLeakFix.h"
|
||||||
|
#include "Patches/ForceLanguage.h"
|
||||||
|
|
||||||
using namespace SKSE;
|
using namespace SKSE;
|
||||||
|
|
||||||
@ -214,6 +215,8 @@ SKSEPluginLoad(const LoadInterface* skse) {
|
|||||||
|
|
||||||
RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr;
|
RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr;
|
||||||
|
|
||||||
|
ForceLanguage::Install("Data/SKSE/Plugins/EnderalLanguage.ini");
|
||||||
|
|
||||||
EventListener::Install();
|
EventListener::Install();
|
||||||
|
|
||||||
GetPapyrusInterface()->Register(Papyrus::Bind);
|
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