diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e758f327 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,72 @@ + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_null_propagation = true:suggestion +indent_style = tab + +[*.cs] +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_indent_labels = one_less_than_current \ No newline at end of file diff --git a/OWML.sln b/OWML.sln index 9e523705..558f2bd0 100644 --- a/OWML.sln +++ b/OWML.sln @@ -62,6 +62,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OWML.ExampleAPI", "src\Samp EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C7F76E72-1CF2-4C0D-8A39-3D13EB868119}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig LICENSE = LICENSE .github\workflows\main.yml = .github\workflows\main.yml owmllogo.png = owmllogo.png @@ -209,7 +210,7 @@ Global {739D16FB-7848-4047-A173-500CE7C40399} = {C447A599-2700-44E1-BBFA-52880B7BFFBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.2.1.505.0\lib\NET35 SolutionGuid = {0E767163-75F9-420A-80EB-320429543CAD} + EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.2.1.505.0\lib\NET35 EndGlobalSection EndGlobal diff --git a/docs/content/pages/guides/mod_settings.md b/docs/content/pages/guides/mod_settings.md index 4a77caca..5f15b800 100644 --- a/docs/content/pages/guides/mod_settings.md +++ b/docs/content/pages/guides/mod_settings.md @@ -138,3 +138,22 @@ public class MyMod : ModBehaviour { ## Config Updates Something important to note is that when the manager pulls and update for your mod, the `config.json` file is preserved. The issue with this is menus are generated from the `config.json` file. When changing options like slider minimums and maximums or choices, you may want to create a new property rather than edit an existing one to make sure the UI is correct. + +## Translations + +Mod config options can be translated in the same way that [New Horizons mods do translations](https://nh.outerwildsmods.com/guides/translation/). First, you need to add a folder named `translations` in the root directory of your mod. + +There are 12 supported languages in Outer Wilds: `english`, `spanish_la`, `german`, `french`, `italian`, `polish`, `portuguese_br`, `japanese`, `russian`, `chinese_simple`, `korean`, and `turkish`. + +In the `translations` folder you can put json files with the name of the language you want to translate for. This file will contain a single dictionary named `UIDictionary` which will have key-value pairs for the translation. + +```json +{ + "UIDictionary": { + "settingTitle": "My Translated Title", + "settingTooltip": "My Translated Tooltip" + } +} +``` + +If your mod already uses New Horizons and supports translations, these values are added directly into the same translation files that NH uses. diff --git a/src/OWML.Common/Interfaces/IModHelper.cs b/src/OWML.Common/Interfaces/IModHelper.cs index 69897b66..41b1ca80 100644 --- a/src/OWML.Common/Interfaces/IModHelper.cs +++ b/src/OWML.Common/Interfaces/IModHelper.cs @@ -1,4 +1,5 @@ -using OWML.Common.Menus; +using OWML.Common.Interfaces; +using OWML.Common.Menus; using System; namespace OWML.Common @@ -31,5 +32,7 @@ public interface IModHelper IModInteraction Interaction { get; } IMenuManager MenuHelper { get; } + + IModTranslations MenuTranslations { get; } } } diff --git a/src/OWML.Common/Interfaces/IModTranslations.cs b/src/OWML.Common/Interfaces/IModTranslations.cs new file mode 100644 index 00000000..2dd5b3ad --- /dev/null +++ b/src/OWML.Common/Interfaces/IModTranslations.cs @@ -0,0 +1,7 @@ +namespace OWML.Common.Interfaces +{ + public interface IModTranslations + { + public string GetLocalizedString(string key); + } +} diff --git a/src/OWML.Launcher/OWML.Manifest.json b/src/OWML.Launcher/OWML.Manifest.json index 46508e60..cf6724f3 100644 --- a/src/OWML.Launcher/OWML.Manifest.json +++ b/src/OWML.Launcher/OWML.Manifest.json @@ -3,7 +3,7 @@ "author": "Alek", "name": "OWML", "uniqueName": "Alek.OWML", - "version": "2.11.1", + "version": "2.12.0", "minGameVersion": "1.1.14.768", "maxGameVersion": "1.1.14.768" } diff --git a/src/OWML.ModHelper.Menus/ModConfigMenuBase.cs b/src/OWML.ModHelper.Menus/ModConfigMenuBase.cs index ce39fbcf..33182cbf 100644 --- a/src/OWML.ModHelper.Menus/ModConfigMenuBase.cs +++ b/src/OWML.ModHelper.Menus/ModConfigMenuBase.cs @@ -3,6 +3,7 @@ using OWML.Common; using OWML.Common.Menus; using OWML.Utils; +using OWML.Common.Interfaces; namespace OWML.ModHelper.Menus { @@ -19,6 +20,8 @@ public abstract class ModConfigMenuBase : ModMenuWithSelectables, IModConfigMenu private IModNumberInput _numberInputTemplate; private IModSeparator _seperatorTemplate; + private IModTranslations _translations; + protected abstract void AddInputs(); public abstract void UpdateUIValues(); @@ -28,6 +31,8 @@ protected ModConfigMenuBase(IModManifest manifest, IModStorage storage, IModCons { Manifest = manifest; Storage = storage; + + _translations = new ModTranslations(manifest, console); } public void Initialize(Menu menu, IModToggleInput toggleTemplate, IModSliderInput sliderTemplate, @@ -111,7 +116,7 @@ private void AddToggleInput(string key, int index, JObject obj = null) { var toggle = AddToggleInput(_toggleTemplate.Copy(key), index); toggle.Element.name = key; - toggle.Title = (string)obj?["title"] ?? key; + SetupTitle(toggle, (string)obj?["title"], key); SetupInputTooltip(toggle, (string)obj?["tooltip"]); toggle.Show(); } @@ -122,7 +127,7 @@ private void AddSliderInput(string key, int index, JObject obj) slider.Min = (float)obj["min"]; slider.Max = (float)obj["max"]; slider.Element.name = key; - slider.Title = (string)obj["title"] ?? key; + SetupTitle(slider, (string)obj?["title"], key); SetupInputTooltip(slider, (string)obj["tooltip"]); slider.Show(); } @@ -132,7 +137,7 @@ private void AddSelectorInput(string key, int index, JObject obj) var options = obj["options"].ToObject(); var selector = AddSelectorInput(_selectorTemplate.Copy(key), index); selector.Element.name = key; - selector.Title = (string)obj["title"] ?? key; + SetupTitle(selector, (string)obj?["title"], key); selector.Initialize((string)obj["value"], options); SetupInputTooltip(selector, (string)obj["tooltip"]); selector.Show(); @@ -142,7 +147,7 @@ private void AddTextInput(string key, int index, JObject obj = null) { var textInput = AddTextInput(_textInputTemplate.Copy(key), index); textInput.Element.name = key; - textInput.Title = (string)obj?["title"] ?? key; + SetupTitle(textInput, (string)obj?["title"], key); SetupInputTooltip(textInput, (string)obj?["tooltip"]); textInput.Show(); } @@ -151,7 +156,7 @@ private void AddNumberInput(string key, int index, JObject obj = null) { var numberInput = AddNumberInput(_numberInputTemplate.Copy(key), index); numberInput.Element.name = key; - numberInput.Title = (string)obj?["title"] ?? key; + SetupTitle(numberInput, (string)obj?["title"], key); SetupInputTooltip(numberInput, (string)obj?["tooltip"]); numberInput.Show(); } @@ -160,7 +165,7 @@ private void AddSeparator(string key, int index, JObject obj) { var separator = AddSeparator(_seperatorTemplate.Copy("Inputs"), index); separator.Element.name = key; - separator.Title = (string)obj?["title"] ?? key; + SetupTitle(separator, (string)obj?["title"], key); separator.Show(); } @@ -168,7 +173,12 @@ internal void SetupInputTooltip(IModInput input, string tooltip) { var menuOption = input.Element.GetComponent(); menuOption.SetValue("_tooltipTextType", UITextType.None); - menuOption.SetValue("_overrideTooltipText", tooltip?? ""); + menuOption.SetValue("_overrideTooltipText", _translations.GetLocalizedString(tooltip) ?? ""); + } + + internal void SetupTitle(IModInputBase input, string title, string key) + { + input.Title = title == null ? key : _translations.GetLocalizedString(title); } } } diff --git a/src/OWML.ModHelper.Menus/ModTranslations.cs b/src/OWML.ModHelper.Menus/ModTranslations.cs new file mode 100644 index 00000000..e8edc610 --- /dev/null +++ b/src/OWML.ModHelper.Menus/ModTranslations.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json.Linq; +using OWML.Common; +using OWML.Common.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; + +namespace OWML.ModHelper.Menus +{ + public class ModTranslations : IModTranslations + { + private Dictionary> _translationTable = new(); + + private IModManifest _manifest; + private IModConsole _console; + + // Menu translations are stored under UIDictionary + // This means OWML config translations follow the New Horizons format + public static readonly string UIDictionary = nameof(UIDictionary); + + private bool _initialized; + + public ModTranslations(IModManifest manifest, IModConsole console) + { + _manifest = manifest; + _console = console; + } + + private void Init() + { + try + { + var translationsFolder = Path.Combine(_manifest.ModFolderPath, "translations"); + foreach (TextTranslation.Language translation in Enum.GetValues(typeof(TextTranslation.Language))) + { + var filename = Path.Combine(translationsFolder, $"{translation}.json"); + if (File.Exists(filename)) + { + var dict = JObject.Parse(File.ReadAllText(filename)).ToObject>(); + if (dict.ContainsKey(UIDictionary)) + { + _translationTable[translation] = (Dictionary)(dict[nameof(UIDictionary)] as JObject).ToObject(typeof(Dictionary)); + } + } + } + _initialized = true; + } + catch (Exception ex) + { + _console.WriteLine($"Failed to initialize mod option translations {ex}", MessageType.Error); + } + } + + public string GetLocalizedString(string key) + { + if (!_initialized) + { + Init(); + } + + try + { + if (key == null) return null; + if (key == string.Empty) return string.Empty; + + if (!_translationTable.TryGetValue(TextTranslation.Get().m_language, out var dict)) + { + // Default to English + if (!_translationTable.TryGetValue(TextTranslation.Language.ENGLISH, out dict)) + { + // Default to key + return key; + } + } + + if (dict.TryGetValue(key, out var value)) + { + return value; + } + else + { + return key; + } + } + catch (Exception ex) + { + _console.WriteLine($"Failed to load options translation: {ex}", MessageType.Error); + return key; + } + } + } +} diff --git a/src/OWML.ModHelper.Menus/NewMenuSystem/MenuManager.cs b/src/OWML.ModHelper.Menus/NewMenuSystem/MenuManager.cs index f16142ff..42bdc755 100644 --- a/src/OWML.ModHelper.Menus/NewMenuSystem/MenuManager.cs +++ b/src/OWML.ModHelper.Menus/NewMenuSystem/MenuManager.cs @@ -216,21 +216,29 @@ void SaveConfig() foreach (var (name, setting) in mod.ModHelper.Config.Settings) { var settingType = GetSettingType(setting); - var label = name; + var label = mod.ModHelper.MenuTranslations.GetLocalizedString(name); var tooltip = ""; var settingObject = setting as JObject; + if (settingObject["dlcOnly"].ToObject()) + { + if (EntitlementsManager.IsDlcOwned() == EntitlementsManager.AsyncOwnershipStatus.NotOwned) + { + continue; + } + } + if (settingObject != default(JObject)) { if (settingObject["title"] != null) { - label = settingObject["title"].ToString(); + label = mod.ModHelper.MenuTranslations.GetLocalizedString(settingObject["title"].ToString()); } if (settingObject["tooltip"] != null) { - tooltip = settingObject["tooltip"].ToString(); + tooltip = mod.ModHelper.MenuTranslations.GetLocalizedString(settingObject["tooltip"].ToString()); } } diff --git a/src/OWML.ModHelper/ModHelper.cs b/src/OWML.ModHelper/ModHelper.cs index 602118a6..1b205e01 100644 --- a/src/OWML.ModHelper/ModHelper.cs +++ b/src/OWML.ModHelper/ModHelper.cs @@ -1,4 +1,5 @@ using OWML.Common; +using OWML.Common.Interfaces; using OWML.Common.Menus; namespace OWML.ModHelper @@ -31,6 +32,8 @@ public class ModHelper : IModHelper public IMenuManager MenuHelper { get; } + public IModTranslations MenuTranslations { get; } + public ModHelper( IModLogger logger, IModConsole console, @@ -44,7 +47,8 @@ public ModHelper( IModDefaultConfig defaultConfig, IOwmlConfig owmlConfig, IModInteraction interaction, - IMenuManager menuHelper) + IMenuManager menuHelper, + IModTranslations menuTranslations) { Logger = logger; Console = console; @@ -59,6 +63,7 @@ public ModHelper( OwmlConfig = owmlConfig; Interaction = interaction; MenuHelper = menuHelper; + MenuTranslations = menuTranslations; } } } diff --git a/src/OWML.ModLoader/ModData.cs b/src/OWML.ModLoader/ModData.cs index 01bfb729..8ca011b4 100644 --- a/src/OWML.ModLoader/ModData.cs +++ b/src/OWML.ModLoader/ModData.cs @@ -77,6 +77,27 @@ private bool MakeConfigConsistentWithDefault() } } + // Fix Titles and Tooltips + // If a mod update changes the title or tooltip in the default-config, this change does not carry over to an existing config.json file + // This data shouldn't even be stored in the config, but whatever! + foreach (var key in keysCopy) + { + if (Config.Settings[key] is JObject configSetting && DefaultConfig.Settings[key] is JObject defaultSetting) + { + configSetting.Remove("title"); + configSetting.Remove("tooltip"); + if (defaultSetting.GetValue("title") != null) + { + configSetting["title"] = defaultSetting.GetValue("title"); + } + if (defaultSetting.GetValue("tooltip") != null) + { + configSetting["tooltip"] = defaultSetting.GetValue("tooltip"); + } + Config.Settings[key] = configSetting; + } + } + AddMissingDefaults(DefaultConfig); ReorderSettings(DefaultConfig); return wasCompatible; diff --git a/src/OWML.ModLoader/Owo.cs b/src/OWML.ModLoader/Owo.cs index 1f0793c4..fb50b22e 100644 --- a/src/OWML.ModLoader/Owo.cs +++ b/src/OWML.ModLoader/Owo.cs @@ -11,6 +11,7 @@ using OWML.ModHelper.Events; using OWML.ModHelper.Input; using OWML.ModHelper.Interaction; +using OWML.ModHelper.Menus; using OWML.Utils; using System; using System.Collections.Generic; @@ -245,6 +246,7 @@ private IModHelper CreateModHelper(IModData modData) => .Add() .Add() .Add() + .Add() .Add() .Resolve();