Skip to content

Commit f80d8a6

Browse files
committed
chore(release): merge dev into main for v0.2.40
2 parents ce692c5 + 19de63c commit f80d8a6

8 files changed

Lines changed: 214 additions & 23 deletions

Const.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static class Const
2222
/// Assembly / manifest version string.
2323
/// 程序集/清单版本字符串。
2424
/// </summary>
25-
public const string Version = "0.2.39";
25+
public const string Version = "0.2.40";
2626

2727
/// <summary>
2828
/// Root key for RitsuLib JSON settings under the mod’s user folder.

Diagnostics/DevConsole/DevConsoleAutocompleteCandidateSources.cs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,40 +68,93 @@ public static IEnumerable<string> GetEpochEntryIds()
6868
public static IEnumerable<(string EntryId, LocString Title)> EnumerateLocalizedModelTitles()
6969
{
7070
foreach (var card in ModelDb.AllCards)
71-
yield return (card.Id.Entry, card.TitleLocString);
71+
yield return GetTitleCandidate(card);
7272

7373
foreach (var potion in ModelDb.AllPotions)
74-
yield return (potion.Id.Entry, potion.Title);
74+
yield return GetTitleCandidate(potion);
7575

7676
foreach (var relic in ModelDb.AllRelics)
77-
yield return (relic.Id.Entry, relic.Title);
77+
yield return GetTitleCandidate(relic);
7878

7979
foreach (var encounter in ModelDb.AllEncounters)
80-
yield return (encounter.Id.Entry, encounter.Title);
80+
yield return GetTitleCandidate(encounter);
8181

8282
foreach (var affliction in ModelDb.DebugAfflictions)
83-
yield return (affliction.Id.Entry, affliction.Title);
83+
yield return GetTitleCandidate(affliction);
8484

8585
foreach (var enchantment in ModelDb.DebugEnchantments)
86-
yield return (enchantment.Id.Entry, enchantment.Title);
86+
yield return GetTitleCandidate(enchantment);
8787

8888
foreach (var ancient in ModelDb.AllAncients)
89-
yield return (ancient.Id.Entry, ancient.Title);
89+
yield return GetTitleCandidate(ancient);
9090

9191
foreach (var evt in ModelDb.AllEvents)
92-
yield return (evt.Id.Entry, evt.Title);
92+
yield return GetTitleCandidate(evt);
9393

9494
foreach (var act in ModelDb.Acts)
95-
yield return (act.Id.Entry, act.Title);
95+
yield return GetTitleCandidate(act);
9696

97-
foreach (var power in ModelDb.AllAbstractModelSubtypes.Where(t => t.IsSubclassOf(typeof(PowerModel))))
98-
yield return (ModelDb.DebugPower(power).Id.Entry, ModelDb.DebugPower(power).Title);
97+
foreach (var powerType in ModelDb.AllAbstractModelSubtypes.Where(t => t.IsSubclassOf(typeof(PowerModel))))
98+
{
99+
var power = ModelDb.DebugPower(powerType);
100+
yield return GetTitleCandidate(power);
101+
}
99102

100103
foreach (var monster in ModelDb.Monsters)
101-
yield return (monster.Id.Entry, monster.Title);
104+
yield return GetTitleCandidate(monster);
102105

103106
foreach (var epochId in EpochModel.AllEpochIds)
104107
yield return (epochId, new("epochs", epochId + ".title"));
105108
}
109+
110+
private static (string EntryId, LocString Title) GetTitleCandidate(AbstractModel model)
111+
{
112+
return (model.Id.Entry, ResolveTitleForAutocomplete(model));
113+
}
114+
115+
private static LocString ResolveTitleForAutocomplete(
116+
AbstractModel model,
117+
HashSet<Type>? visitedModelTypes = null)
118+
{
119+
if (model is not ITemporaryPower temporaryPower)
120+
return DefaultTitleForAutocomplete(model);
121+
122+
visitedModelTypes ??= [];
123+
if (!visitedModelTypes.Add(model.GetType()))
124+
return DefaultTitleForAutocomplete(model);
125+
126+
try
127+
{
128+
var origin = temporaryPower.OriginModel;
129+
return ReferenceEquals(origin, model)
130+
? DefaultTitleForAutocomplete(model)
131+
: ResolveTitleForAutocomplete(origin, visitedModelTypes);
132+
}
133+
catch
134+
{
135+
return DefaultTitleForAutocomplete(model);
136+
}
137+
}
138+
139+
private static LocString DefaultTitleForAutocomplete(AbstractModel model)
140+
{
141+
return model switch
142+
{
143+
CardModel => new("cards", model.Id.Entry + ".title"),
144+
PotionModel => new("potions", model.Id.Entry + ".title"),
145+
RelicModel => new("relics", model.Id.Entry + ".title"),
146+
EncounterModel => new("encounters", model.Id.Entry + ".title"),
147+
AfflictionModel => new("afflictions", model.Id.Entry + ".title"),
148+
EnchantmentModel => new("enchantments", model.Id.Entry + ".title"),
149+
EventModel => new("events", model.Id.Entry + ".title"),
150+
ActModel => new("acts", model.Id.Entry + ".title"),
151+
PowerModel => new("powers", model.Id.Entry + ".title"),
152+
MonsterModel => new("monsters", model.Id.Entry + ".name"),
153+
OrbModel => new("orbs", model.Id.Entry + ".title"),
154+
CharacterModel => new("characters", model.Id.Entry + ".title"),
155+
ModifierModel => new("modifiers", model.Id.Entry + ".title"),
156+
_ => new(model.Id.Category.ToLowerInvariant(), model.Id.Entry + ".title"),
157+
};
158+
}
106159
}
107160
}

Localization/I18NLocTable.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using MegaCrit.Sts2.Core.Localization;
2+
using STS2RitsuLib.Utils;
3+
4+
namespace STS2RitsuLib.Localization
5+
{
6+
internal sealed class I18NLocTable(string name, I18N i18N) : LocTable(name, [])
7+
{
8+
internal string Name { get; } = name;
9+
10+
internal I18N I18N { get; } = i18N;
11+
}
12+
}

Localization/I18NLocTableBridge.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Concurrent;
2+
using MegaCrit.Sts2.Core.Localization;
23
using STS2RitsuLib.Content;
34
using STS2RitsuLib.Utils;
45

@@ -15,6 +16,9 @@ public static class I18NLocTableBridge
1516
private static readonly ConcurrentDictionary<string, I18N> Tables =
1617
new(StringComparer.OrdinalIgnoreCase);
1718

19+
private static readonly ConcurrentDictionary<string, I18NLocTable> LocTables =
20+
new(StringComparer.OrdinalIgnoreCase);
21+
1822
/// <summary>
1923
/// Builds a virtual localization table id using the framework's standard three-segment convention:
2024
/// <c>MODID_I18N_STEM</c>.
@@ -40,9 +44,16 @@ public static bool TryRegister(string modId, I18N i18N, string stem = "DEFAULT",
4044
var tableId = GetTableId(modId, stem);
4145

4246
if (!replaceExisting)
43-
return Tables.TryAdd(tableId, i18N);
47+
{
48+
var added = Tables.TryAdd(tableId, i18N);
49+
if (added)
50+
LocTables.TryAdd(tableId, new(tableId, i18N));
51+
52+
return added;
53+
}
4454

4555
Tables[tableId] = i18N;
56+
LocTables[tableId] = new(tableId, i18N);
4657
return true;
4758
}
4859

@@ -54,13 +65,30 @@ public static bool TryUnregister(string modId, string stem = "DEFAULT")
5465
{
5566
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
5667
ArgumentException.ThrowIfNullOrWhiteSpace(stem);
57-
return Tables.TryRemove(GetTableId(modId, stem), out _);
68+
69+
var tableId = GetTableId(modId, stem);
70+
var removed = Tables.TryRemove(tableId, out _);
71+
if (removed)
72+
LocTables.TryRemove(tableId, out _);
73+
74+
return removed;
5875
}
5976

6077
internal static bool TryGet(string tableId, out I18N i18N)
6178
{
6279
ArgumentException.ThrowIfNullOrWhiteSpace(tableId);
6380
return Tables.TryGetValue(tableId, out i18N!);
6481
}
82+
83+
internal static bool TryGetLocTable(string tableId, out LocTable locTable)
84+
{
85+
locTable = null!;
86+
87+
if (!Tables.TryGetValue(tableId, out var i18N))
88+
return false;
89+
90+
locTable = LocTables.GetOrAdd(tableId, static (id, backingI18N) => new(id, backingI18N), i18N);
91+
return true;
92+
}
6593
}
6694
}

Localization/Patches/I18NLocTableBridgePatches.cs

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,62 @@
11
using HarmonyLib;
22
using MegaCrit.Sts2.Core.Localization;
33
using STS2RitsuLib.Patching.Models;
4+
using STS2RitsuLib.Utils;
45

56
namespace STS2RitsuLib.Localization.Patches
67
{
8+
internal static class I18NLocTablePatchHelper
9+
{
10+
internal static bool TryGetBackingI18N(LocTable table, out I18N i18N)
11+
{
12+
if (table is not I18NLocTable i18NLocTable)
13+
return I18NLocTableBridge.TryGet(LocTableCompatibilityPatchHelper.GetTableName(table), out i18N);
14+
i18N = i18NLocTable.I18N;
15+
return true;
16+
}
17+
}
18+
19+
/// <summary>
20+
/// Resolves registered virtual I18N tables through <c>LocManager.GetTable</c>.
21+
/// 通过 <c>LocManager.GetTable</c> 解析已注册的虚拟 I18N table。
22+
/// </summary>
23+
public class LocManagerGetTableI18NBridgePatch : IPatchMethod
24+
{
25+
/// <inheritdoc />
26+
public static string PatchId => "loc_manager_get_table_i18n_bridge";
27+
28+
/// <inheritdoc />
29+
public static string Description => "Resolve registered I18N virtual tables from LocManager.GetTable";
30+
31+
/// <inheritdoc />
32+
public static bool IsCritical => true;
33+
34+
/// <inheritdoc />
35+
public static ModPatchTarget[] GetTargets()
36+
{
37+
return
38+
[
39+
new(typeof(LocManager), nameof(LocManager.GetTable), [typeof(string)]),
40+
];
41+
}
42+
43+
// ReSharper disable InconsistentNaming
44+
/// <summary>
45+
/// Returns the I18N-backed table instance for registered virtual table ids.
46+
/// 对已注册的虚拟 table id 返回 I18N-backed table 实例。
47+
/// </summary>
48+
[HarmonyPriority(Priority.First)]
49+
public static bool Prefix(string name, ref LocTable __result)
50+
// ReSharper restore InconsistentNaming
51+
{
52+
if (!I18NLocTableBridge.TryGetLocTable(name, out var locTable))
53+
return true;
54+
55+
__result = locTable;
56+
return false;
57+
}
58+
}
59+
760
/// <summary>
861
/// Bridges <c>LocTable.HasEntry</c> to <see cref="I18NLocTableBridge" /> for virtual <c>MODID_I18N_STEM</c> tables.
962
/// 将 <c>LocTable.HasEntry</c> 桥接到 <see cref="I18NLocTableBridge" />,用于虚拟 <c>MODID_I18N_STEM</c> table。
@@ -39,8 +92,50 @@ public static ModPatchTarget[] GetTargets()
3992
public static bool Prefix(LocTable __instance, string key, ref bool __result)
4093
// ReSharper restore InconsistentNaming
4194
{
42-
var tableName = LocTableCompatibilityPatchHelper.GetTableName(__instance);
43-
if (!I18NLocTableBridge.TryGet(tableName, out var i18N))
95+
if (!I18NLocTablePatchHelper.TryGetBackingI18N(__instance, out var i18N))
96+
return true;
97+
98+
__result = i18N.ContainsKey(key);
99+
return false;
100+
}
101+
}
102+
103+
/// <summary>
104+
/// Bridges <c>LocTable.IsLocalKey</c> to <see cref="I18NLocTableBridge" /> for virtual <c>MODID_I18N_STEM</c>
105+
/// tables.
106+
/// 将 <c>LocTable.IsLocalKey</c> 桥接到 <see cref="I18NLocTableBridge" />,用于虚拟 <c>MODID_I18N_STEM</c>
107+
/// table。
108+
/// </summary>
109+
public class LocTableIsLocalKeyI18NBridgePatch : IPatchMethod
110+
{
111+
/// <inheritdoc />
112+
public static string PatchId => "loc_table_is_local_key_i18n_bridge";
113+
114+
/// <inheritdoc />
115+
public static string Description => "Resolve LocTable.IsLocalKey via registered I18N virtual tables";
116+
117+
/// <inheritdoc />
118+
public static bool IsCritical => true;
119+
120+
/// <inheritdoc />
121+
public static ModPatchTarget[] GetTargets()
122+
{
123+
return
124+
[
125+
new(typeof(LocTable), nameof(LocTable.IsLocalKey), [typeof(string)]),
126+
];
127+
}
128+
129+
// ReSharper disable InconsistentNaming
130+
/// <summary>
131+
/// Reports I18N-backed keys as local keys for SmartFormat culture selection.
132+
/// 为 SmartFormat culture 选择把 I18N-backed keys 报告为本地 key。
133+
/// </summary>
134+
[HarmonyPriority(Priority.First)]
135+
public static bool Prefix(LocTable __instance, string key, ref bool __result)
136+
// ReSharper restore InconsistentNaming
137+
{
138+
if (!I18NLocTablePatchHelper.TryGetBackingI18N(__instance, out var i18N))
44139
return true;
45140

46141
__result = i18N.ContainsKey(key);
@@ -81,8 +176,7 @@ public static ModPatchTarget[] GetTargets()
81176
public static bool Prefix(LocTable __instance, string key, ref string __result)
82177
// ReSharper restore InconsistentNaming
83178
{
84-
var tableName = LocTableCompatibilityPatchHelper.GetTableName(__instance);
85-
if (!I18NLocTableBridge.TryGet(tableName, out var i18N))
179+
if (!I18NLocTablePatchHelper.TryGetBackingI18N(__instance, out var i18N))
86180
return true;
87181

88182
if (!i18N.TryGet(key, out var text))
@@ -128,8 +222,10 @@ public static ModPatchTarget[] GetTargets()
128222
public static bool Prefix(LocTable __instance, string key, ref LocString __result)
129223
// ReSharper restore InconsistentNaming
130224
{
131-
var tableName = LocTableCompatibilityPatchHelper.GetTableName(__instance);
132-
if (!I18NLocTableBridge.TryGet(tableName, out var i18N))
225+
var tableName = __instance is I18NLocTable i18NLocTable
226+
? i18NLocTable.Name
227+
: LocTableCompatibilityPatchHelper.GetTableName(__instance);
228+
if (!I18NLocTablePatchHelper.TryGetBackingI18N(__instance, out var i18N))
133229
return true;
134230

135231
if (!i18N.ContainsKey(key))

RitsuLibFramework.PatcherSetup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ private static void RegisterLifecyclePatches()
136136
patcher.RegisterPatch<RitsuLibMobileModelDbInitPostfixPatch>();
137137
else
138138
patcher.RegisterPatch<NDailyRunLoadScreenBeginRunMissingCharacterPatch>();
139+
patcher.RegisterPatch<LocManagerGetTableI18NBridgePatch>();
139140
patcher.RegisterPatch<LocTableHasEntryI18NBridgePatch>();
141+
patcher.RegisterPatch<LocTableIsLocalKeyI18NBridgePatch>();
140142
patcher.RegisterPatch<LocTableGetRawTextI18NBridgePatch>();
141143
patcher.RegisterPatch<LocTableGetLocStringI18NBridgePatch>();
142144
patcher.RegisterPatch<LocTableGetLocStringCompatibilityPatch>();

STS2-RitsuLib.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
<PropertyGroup Label="NuGet package">
3535
<IsPackable>true</IsPackable>
36-
<Version>0.2.39</Version>
36+
<Version>0.2.40</Version>
3737
<Authors>OLC</Authors>
3838
<Description>Shared framework library for Slay the Spire 2 mods.</Description>
3939
<PackageReadmeFile>README.md</PackageReadmeFile>

mod_manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "RitsuLib",
55
"author": "OLC",
66
"description": "A shared Slay the Spire 2 mod framework library providing reusable patching, persistence, lifecycle, localization, and utility APIs for other mods.",
7-
"version": "0.2.39",
7+
"version": "0.2.40",
88
"has_pck": false,
99
"has_dll": true,
1010
"affects_gameplay": false,

0 commit comments

Comments
 (0)