-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathRitsuLibFramework.cs
More file actions
1118 lines (979 loc) · 47.2 KB
/
RitsuLibFramework.cs
File metadata and controls
1118 lines (979 loc) · 47.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System.Collections.Concurrent;
using System.Reflection;
using Godot;
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Entities.Players;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Modding;
using MegaCrit.Sts2.Core.Models;
using STS2RitsuLib.CardPiles;
using STS2RitsuLib.Cards.FreePlay;
using STS2RitsuLib.CardTags;
using STS2RitsuLib.Combat.HandSize;
using STS2RitsuLib.Combat.HealthBars;
using STS2RitsuLib.Compat;
using STS2RitsuLib.Content;
using STS2RitsuLib.Data;
using STS2RitsuLib.Diagnostics.CardExport;
using STS2RitsuLib.Diagnostics.CompendiumExport;
using STS2RitsuLib.Interop;
using STS2RitsuLib.Keywords;
using STS2RitsuLib.Localization;
using STS2RitsuLib.Localization.SmartFormat;
using STS2RitsuLib.Patching.Core;
using STS2RitsuLib.Platform;
using STS2RitsuLib.RuntimeInput;
using STS2RitsuLib.Scaffolding.Ancients.Options;
using STS2RitsuLib.Scaffolding.Content;
using STS2RitsuLib.Settings;
using STS2RitsuLib.Settings.RunSidecar;
using STS2RitsuLib.Timeline;
using STS2RitsuLib.TopBar;
using STS2RitsuLib.Ui.Toast;
using STS2RitsuLib.Unlocks;
using STS2RitsuLib.Utils;
using STS2RitsuLib.Utils.Persistence;
using Logger = MegaCrit.Sts2.Core.Logging.Logger;
namespace STS2RitsuLib
{
/// <summary>
/// Shared runtime bootstrap for the framework itself and for mods that reference it.
/// 框架自身以及引用该框架的 Mod 共用的运行时启动入口。
/// </summary>
[ModInitializer(nameof(Initialize))]
public static partial class RitsuLibFramework
{
private static readonly Lock SyncRoot = new();
private static readonly Dictionary<FrameworkPatcherArea, ModPatcher> FrameworkPatchersByArea = [];
private static bool _frameworkInteropBootstrapRegistered;
private static bool _profileServicesInitialized;
private static ILifecycleObserver[] _lifecycleObservers = [];
private static readonly ConcurrentDictionary<Type, object> LifecycleTopics = new();
private static readonly Dictionary<Type, object> ReplayableLifecycleEvents = [];
private static readonly HashSet<string> RegisteredScriptAssemblies = [];
private static readonly Lock DeferredContentPackSync = new();
private static readonly List<DeferredContentPackRegistration> DeferredContentPackRegistrations = [];
private static bool _deferredContentPacksFlushed;
static RitsuLibFramework()
{
Logger = CreateLogger(Const.ModId);
}
/// <summary>
/// Framework logger instance (typed as <c>MegaCrit.Sts2.Core.Logging.Logger</c>).
/// 框架 logger 实例(类型为 <c>MegaCrit.Sts2.Core.Logging.Logger</c>)。
/// </summary>
public static Logger Logger { get; private set; }
/// <summary>
/// True after <see cref="Initialize" /> completes without a fatal patch failure.
/// <see cref="Initialize" /> 在没有致命 patch 失败的情况下完成后为 True。
/// </summary>
public static bool IsInitialized { get; private set; }
/// <summary>
/// True when the framework finished initialization and critical patches succeeded.
/// 当框架完成初始化且关键补丁成功应用时为 true。
/// </summary>
public static bool IsActive { get; private set; }
/// <summary>
/// True when at least one mod has registered a settings page via <see cref="RegisterModSettings" />.
/// 至少一个 mod 已通过 <see cref="RegisterModSettings" /> 注册设置页时为 True。
/// </summary>
public static bool HasRegisteredModSettings => ModSettingsRegistry.HasPages;
/// <summary>
/// Subscribes an observer to framework lifecycle events, optionally replaying the current replayable state.
/// 订阅框架生命周期事件观察者,并可选择回放当前可回放状态。
/// </summary>
/// <param name="observer">
/// Receives lifecycle notifications via <c>OnEvent</c>.
/// 通过 <c>OnEvent</c> 接收生命周期通知。
/// </param>
/// <param name="replayCurrentState">
/// When true, dispatches replayable events that already occurred.
/// 为 true 时,派发已经发生过的可回放事件。
/// </param>
/// <returns>
/// Disposing unsubscribes the observer.
/// 释放返回值会取消订阅该观察者。
/// </returns>
public static IDisposable SubscribeLifecycle(ILifecycleObserver observer, bool replayCurrentState = true)
{
ArgumentNullException.ThrowIfNull(observer);
IFrameworkLifecycleEvent[] lifecycleSnapshot;
lock (SyncRoot)
{
_lifecycleObservers = AppendItem(_lifecycleObservers, observer);
lifecycleSnapshot = replayCurrentState
? ReplayableLifecycleEvents.Values
.Cast<IFrameworkLifecycleEvent>()
.OrderBy(evt => evt.OccurredAtUtc)
.ToArray()
: [];
}
foreach (var evt in lifecycleSnapshot)
SafeNotify(observer, evt, evt.GetType().Name);
return new FrameworkLifecycleSubscription(() =>
{
lock (SyncRoot)
{
_lifecycleObservers = RemoveItem(_lifecycleObservers, observer);
}
});
}
/// <summary>
/// Subscribes a typed callback for a specific <typeparamref name="TEvent" /> lifecycle event.
/// 为特定 <typeparamref name="TEvent" /> 生命周期事件订阅类型化回调。
/// </summary>
/// <typeparam name="TEvent">
/// Concrete lifecycle event type.
/// 具体的生命周期事件类型。
/// </typeparam>
/// <param name="handler">
/// Invoked for each matching event.
/// 每次匹配事件到达时调用。
/// </param>
/// <param name="replayCurrentState">
/// When true, invokes <paramref name="handler" /> with the last replayable event if
/// present.
/// 为 true 时,如果存在最后一个可重放事件,则用它调用 <paramref name="handler" />。
/// </param>
/// <returns>
/// Disposing unsubscribes the handler.
/// 释放返回值会取消订阅该回调。
/// </returns>
public static IDisposable SubscribeLifecycle<TEvent>(Action<TEvent> handler, bool replayCurrentState = true)
where TEvent : IFrameworkLifecycleEvent
{
ArgumentNullException.ThrowIfNull(handler);
if (!LifecycleEventTypeCache<TEvent>.SupportsTypedDispatch)
return SubscribeLifecycle(new DelegateLifecycleObserver<TEvent>(handler), replayCurrentState);
object? replayEvent = null;
var topic = GetLifecycleTopic<TEvent>();
lock (SyncRoot)
{
topic.Add(handler);
if (replayCurrentState)
ReplayableLifecycleEvents.TryGetValue(LifecycleEventTypeCache<TEvent>.EventType, out replayEvent);
}
if (replayEvent is TEvent typedReplayEvent)
SafeNotify(handler, typedReplayEvent, LifecycleEventTypeCache<TEvent>.EventName);
return new FrameworkLifecycleSubscription(() =>
{
lock (SyncRoot)
{
topic.Remove(handler);
}
});
}
/// <summary>
/// Subscribes a typed callback for a specific <typeparamref name="TEvent" />, passing the same
/// <see cref="IDisposable" /> subscription instance on every invocation (including synchronous replay).
/// 为特定 <typeparamref name="TEvent" /> 订阅类型化回调,并在每次调用时传入同一个
/// <see cref="IDisposable" /> 订阅实例(包括同步重放)。
/// </summary>
/// <typeparam name="TEvent">
/// Concrete lifecycle event type.
/// 具体的生命周期事件类型。
/// </typeparam>
/// <param name="handler">
/// Invoked for each matching event. The <see cref="IDisposable" /> argument is the subscription; disposing it
/// unsubscribes the handler.
/// 对每个匹配事件调用。<see cref="IDisposable" /> 参数是订阅;释放它会
/// 取消订阅该 handler。
/// </param>
/// <param name="replayCurrentState">
/// When true, invokes <paramref name="handler" /> with the last replayable event if present.
/// 为 true 时,如果存在最后一个可重放事件,则用它调用 <paramref name="handler" />。
/// </param>
/// <returns>
/// Disposing unsubscribes the handler.
/// 释放返回值会取消订阅该回调。
/// </returns>
public static IDisposable SubscribeLifecycle<TEvent>(
Action<TEvent, IDisposable> handler,
bool replayCurrentState = true
)
where TEvent : IFrameworkLifecycleEvent
{
ArgumentNullException.ThrowIfNull(handler);
if (!LifecycleEventTypeCache<TEvent>.SupportsTypedDispatch)
{
var holder = new LifecycleSubscriptionHolder();
var observer = new DelegateLifecycleObserverWithSubscription<TEvent>(handler, holder);
IFrameworkLifecycleEvent[] lifecycleSnapshot;
lock (SyncRoot)
{
_lifecycleObservers = AppendItem(_lifecycleObservers, observer);
holder.Subscription = new FrameworkLifecycleSubscription(() =>
{
lock (SyncRoot)
{
_lifecycleObservers = RemoveItem(_lifecycleObservers, observer);
}
});
lifecycleSnapshot = replayCurrentState
? ReplayableLifecycleEvents.Values
.Cast<IFrameworkLifecycleEvent>()
.OrderBy(evt => evt.OccurredAtUtc)
.ToArray()
: [];
}
foreach (var evt in lifecycleSnapshot)
SafeNotify(observer, evt, evt.GetType().Name);
return holder.Subscription;
}
object? replayEvent = null;
var topic = GetLifecycleTopic<TEvent>();
FrameworkLifecycleSubscription? subscription = null;
lock (SyncRoot)
{
subscription = new(() =>
{
lock (SyncRoot)
{
topic.Remove(Wrapped);
}
});
topic.Add(Wrapped);
if (replayCurrentState)
ReplayableLifecycleEvents.TryGetValue(LifecycleEventTypeCache<TEvent>.EventType, out replayEvent);
}
if (replayCurrentState && replayEvent is TEvent typedReplayEvent)
SafeNotify(Wrapped, typedReplayEvent, LifecycleEventTypeCache<TEvent>.EventName);
return subscription;
void Wrapped(TEvent evt)
{
try
{
handler(evt, subscription!);
}
catch (Exception ex)
{
Logger.Warn(
$"[Lifecycle] Observer callback failed in {LifecycleEventTypeCache<TEvent>.EventName}: {ex.Message}"
);
}
}
}
/// <summary>
/// Initializes the shared framework: settings, patch registration, and lifecycle publication.
/// 初始化共享框架,包括设置、补丁注册和生命周期事件发布。
/// </summary>
public static void Initialize()
{
lock (SyncRoot)
{
if (IsInitialized)
{
Logger.Debug("Framework already initialized, skipping duplicate initialization.");
return;
}
Logger = CreateLogger(Const.ModId);
Logger.Info($"Framework ID: {Const.ModId}");
Logger.Info($"Framework Name: {Const.Name}");
Logger.Info(BuildVersionLogText());
Logger.Info("Initializing shared framework...");
RitsuLibMobileSteamRuntime.LogSuppressedSteamFeaturesAtStartup();
ModTypeDiscoveryHub.EnsureBuiltInContributorsRegistered();
RitsuLibSettingsStore.Initialize();
RitsuLibModSettingsBootstrap.Initialize();
PublishLifecycleEvent(
new FrameworkInitializingEvent(Const.ModId, Const.Version, DateTimeOffset.UtcNow),
nameof(FrameworkInitializingEvent)
);
try
{
FrameworkPatchersByArea.Clear();
RegisterLifecyclePatches();
RegisterSettingsUiPatches();
RegisterContentAssetPatches();
RegisterCharacterAssetPatches();
RegisterContentRegistryPatches();
RegisterPersistencePatches();
RegisterUnlockPatches();
if (!PatchAllRequired())
{
Logger.Error("Framework initialization failed: critical framework patches failed.");
IsActive = false;
return;
}
IsInitialized = true;
IsActive = true;
var modDataInteropRegistered = ModDataRuntimeInterop.TryRegisterAll();
if (modDataInteropRegistered > 0)
Logger.Debug(
$"ModData runtime interop: mirror-registered {modDataInteropRegistered} provider schema(s).");
EnsureFrameworkInteropBootstrapRegistered();
RuntimeHotkeyService.Initialize();
RitsuToastService.Initialize();
var frameworkInitializedEvent = new FrameworkInitializedEvent(
Const.ModId,
IsActive,
DateTimeOffset.UtcNow
);
PublishLifecycleEvent(frameworkInitializedEvent, nameof(FrameworkInitializedEvent));
Logger.Info("Shared framework initialization complete.");
}
catch (Exception ex)
{
Logger.Error($"Framework initialization failed: {ex.Message}");
Logger.Error($"Stack trace: {ex.StackTrace}");
IsActive = false;
}
}
}
private static string BuildVersionLogText()
{
var compatBranchLabel = GetCompatBranchLabel();
return string.IsNullOrWhiteSpace(compatBranchLabel)
? $"Version: {Const.Version}"
: $"Version: {Const.Version} [compat branch: {compatBranchLabel}]";
}
private static void EnsureFrameworkInteropBootstrapRegistered()
{
if (_frameworkInteropBootstrapRegistered)
return;
_frameworkInteropBootstrapRegistered = true;
SubscribeLifecycle<DeferredInitializationCompletedEvent>(_ => ConfirmExternalFrameworkInterop());
}
private static void ConfirmExternalFrameworkInterop()
{
ExternalFrameworkRegistry.RefreshKnownFrameworkPresence("deferred initialization completed");
BaseLibHealthBarForecastBridge.TryRegister();
BaseLibVisualGraftBridge.TryRegister();
BaseLibMaxHandSizeBridge.TryInitialize();
MaxHandSizePatchInstaller.EnsurePatched();
}
private static string? GetCompatBranchLabel()
{
#if !STS2_AT_LEAST_0_104_0
return "0.103.2";
#elif !STS2_AT_LEAST_0_105_0
return "0.104.0";
#else
return null;
#endif
}
/// <summary>
/// Ensures profile-bound services (<c>ProfileManager</c>, profile-scoped <c>ModDataStore</c>) are initialized once.
/// 确保与配置档绑定的服务(<c>ProfileManager</c>、配置档作用域的 <c>ModDataStore</c>)只初始化一次。
/// </summary>
public static void EnsureProfileServicesInitialized()
{
lock (SyncRoot)
{
if (_profileServicesInitialized)
return;
PublishLifecycleEvent(
new ProfileServicesInitializingEvent(DateTimeOffset.UtcNow),
nameof(ProfileServicesInitializingEvent)
);
ModDataRuntimeInterop.EnsureProfileSwitchSyncHook();
ProfileManager.Instance.Initialize();
ModDataStore.InitializeAllProfileScoped();
ModDataRuntimeInterop.PushLoadedDataToAllProviders();
ModRunSidecarSession.AttachLifecycleHandlers();
_profileServicesInitialized = true;
var profileInitializedEvent = new ProfileServicesInitializedEvent(
ProfileManager.Instance.CurrentProfileId,
DateTimeOffset.UtcNow
);
PublishLifecycleEvent(profileInitializedEvent, nameof(ProfileServicesInitializedEvent));
Logger.Debug("Profile-scoped framework services initialized.");
}
}
/// <summary>
/// Begins a registration scope for the given mod's <c>ModDataStore</c> entries.
/// 为给定 mod 的 <c>ModDataStore</c> 条目开始注册作用域。
/// </summary>
/// <param name="modId">
/// Owning mod identifier.
/// 所属 Mod 标识符。
/// </param>
/// <param name="initializeProfileIfReady">
/// When true, initializes profile services if the profile is already ready.
/// 为 true 时,如果档案已经就绪,则初始化档案服务。
/// </param>
/// <returns>
/// Disposing ends the registration scope.
/// 释放返回值会结束该注册作用域。
/// </returns>
public static IDisposable BeginModDataRegistration(string modId, bool initializeProfileIfReady = true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
return ModDataStore.For(modId).BeginRegistrationScope(initializeProfileIfReady);
}
/// <summary>
/// Returns the persistent data store facade for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的持久数据存储 facade。
/// </summary>
public static ModDataStore GetDataStore(string modId)
{
return ModDataStore.For(modId);
}
/// <summary>
/// Returns the content registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的内容注册表。
/// </summary>
public static ModContentRegistry GetContentRegistry(string modId)
{
return ModContentRegistry.For(modId);
}
/// <summary>
/// Returns the keyword registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的关键字注册表。
/// </summary>
public static ModKeywordRegistry GetKeywordRegistry(string modId)
{
return ModKeywordRegistry.For(modId);
}
/// <summary>
/// Returns the SmartFormat extension registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的 SmartFormat 扩展注册表。
/// </summary>
public static ModSmartFormatExtensionRegistry GetSmartFormatRegistry(string modId)
{
return ModSmartFormatExtensionRegistry.For(modId);
}
/// <summary>
/// Returns the custom card-tag registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的自定义卡牌标签注册表。
/// </summary>
public static ModCardTagRegistry GetCardTagRegistry(string modId)
{
return ModCardTagRegistry.For(modId);
}
/// <summary>
/// Returns the custom card-pile registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的自定义卡牌牌堆注册表。
/// </summary>
public static ModCardPileRegistry GetCardPileRegistry(string modId)
{
return ModCardPileRegistry.For(modId);
}
/// <summary>
/// Returns the top-bar button registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的顶部栏按钮注册表。
/// </summary>
public static ModTopBarButtonRegistry GetTopBarButtonRegistry(string modId)
{
return ModTopBarButtonRegistry.For(modId);
}
/// <summary>
/// Returns the timeline (epoch/story) registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的时间线(纪元/故事)注册表。
/// </summary>
public static ModTimelineRegistry GetTimelineRegistry(string modId)
{
return ModTimelineRegistry.For(modId);
}
/// <summary>
/// Returns the unlock rules registry for <paramref name="modId" />.
/// 返回 <paramref name="modId" /> 的解锁规则注册表。
/// </summary>
public static ModUnlockRegistry GetUnlockRegistry(string modId)
{
return ModUnlockRegistry.For(modId);
}
/// <summary>
/// Registers a non-power health bar forecast source type through the framework.
/// 通过框架注册非能力生命条预测来源类型。
/// </summary>
public static void RegisterHealthBarForecast<TSource>(string modId, string? sourceId = null)
where TSource : IHealthBarForecastSource, new()
{
HealthBarForecastRegistry.Register<TSource>(modId, sourceId);
}
/// <summary>
/// Registers a non-power health bar visual graft source type through the framework.
/// 通过框架注册非能力生命条视觉 graft 来源类型。
/// </summary>
public static void RegisterHealthBarVisualGraft<TSource>(string modId, string? sourceId = null)
where TSource : IHealthBarVisualGraftSource, new()
{
HealthBarVisualGraftRegistry.Register<TSource>(modId, sourceId);
}
/// <summary>
/// Resolves the current max-hand-size value for <paramref name="player" />.
/// 解析 <paramref name="player" /> 当前的最大手牌数值。
/// </summary>
public static int GetMaxHandSize(Player player)
{
return MaxHandSizeCalculator.Calculate(player);
}
/// <summary>
/// Registers an additional free-play detector used by framework consumers (for example material logic).
/// 注册一个额外的免费打出检测器,供框架消费者使用(例如材质逻辑)。
/// </summary>
public static void RegisterFreePlayBinding(string bindingId, Func<CardPlay, bool> detector)
{
FreePlayBindingRegistry.Register(bindingId, detector);
}
/// <summary>
/// Registers an initial-option injection rule for <typeparamref name="TAncient" />.
/// 为 <typeparamref name="TAncient" /> 注册初始选项注入规则。
/// </summary>
public static void RegisterAncientOption<TAncient>(string modId, ModAncientOptionRule rule)
where TAncient : AncientEventModel
{
GetContentRegistry(modId).RegisterAncientOption<TAncient>(rule);
}
/// <summary>
/// Creates a content pack builder for <paramref name="modId" />.
/// 为 <paramref name="modId" /> 创建内容包构建器。
/// </summary>
public static ModContentPackBuilder CreateContentPack(string modId)
{
return ModContentPackBuilder.For(modId);
}
internal static void EnqueueDeferredContentPack(string modId, Action<ModContentPackContext> apply,
string? description = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
ArgumentNullException.ThrowIfNull(apply);
lock (DeferredContentPackSync)
{
if (_deferredContentPacksFlushed)
throw new InvalidOperationException(
$"Content pack registration for mod '{modId}' is already closed for this run.");
DeferredContentPackRegistrations.Add(new(modId, apply, description));
}
}
internal static void FlushDeferredContentPacks()
{
List<DeferredContentPackRegistration> pending;
lock (DeferredContentPackSync)
{
if (_deferredContentPacksFlushed)
return;
_deferredContentPacksFlushed = true;
pending = [.. DeferredContentPackRegistrations];
DeferredContentPackRegistrations.Clear();
}
if (pending.Count == 0)
return;
pending.Sort(static (x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.ModId, y.ModId));
foreach (var registration in pending)
{
var context = new ModContentPackContext(
registration.ModId,
GetContentRegistry(registration.ModId),
GetKeywordRegistry(registration.ModId),
GetTimelineRegistry(registration.ModId),
GetUnlockRegistry(registration.ModId),
GetCardTagRegistry(registration.ModId),
GetCardPileRegistry(registration.ModId));
registration.Apply(context);
}
Logger.Info($"[ContentPack] Flushed {pending.Count} deferred content pack(s).");
}
/// <summary>
/// Starts a batch PNG export of registered cards (see <see cref="CardPngExporter" />).
/// 开始批量导出已注册卡牌的 PNG(见 <see cref="CardPngExporter" />)。
/// </summary>
/// <param name="request">
/// Output directory, scale, hover panel, filters, etc.
/// 输出目录、缩放、悬停面板、过滤器等导出参数。
/// </param>
/// <param name="issuingPlayer">
/// Optional; export does not require a run or player.
/// 可选参数;导出不要求存在当前 run 或玩家。
/// </param>
public static void BeginCardPngExport(CardPngExportRequest request, Player? issuingPlayer = null)
{
CardPngExporter.BeginExport(request, issuingPlayer, msg => Logger.Info(msg));
}
/// <summary>
/// Starts a batch PNG export of compendium-style detail panels: relic <c>inspect_relic_screen</c> popup, and
/// potion lab focus (scaled <c>NPotion</c> + hovers). Does not use save / unlock gating; content is the “seen
/// unlocked” form.
/// 开始批量导出 compendium 风格的详情面板 PNG:遗物 <c>inspect_relic_screen</c> 弹窗,以及
/// 药水实验室焦点(缩放后的 <c>NPotion</c> + 悬停)。不使用存档/解锁门控;内容为“已见
/// 已解锁”形态。
/// </summary>
public static void BeginCompendiumDetailPngExport(CompendiumPngExportRequest request)
{
CompendiumDetailPngExporter.BeginExport(request, msg => Logger.Info(msg));
}
/// <summary>
/// Declares a <c>mod_data</c> JSON path that may participate in RitsuLib Steam Cloud sync when the player enables
/// it and the session uses Steam Cloud. Prefer ModDataStore.Register when you already use
/// <see cref="Data.ModDataStore" />; this call is for custom persistence that still resolves via
/// <see cref="Utils.Persistence.ProfileManager" />.
/// 声明一个 <c>mod_data</c> JSON 路径;当玩家启用 Steam Cloud 且会话使用 Steam Cloud 时,该路径可以参与 RitsuLib Steam Cloud 同步。
/// 当你已经使用 <see cref="Data.ModDataStore" /> 时,优先使用 ModDataStore.Register;此调用用于仍通过
/// <see cref="Utils.Persistence.ProfileManager" /> 解析的自定义持久化。
/// </summary>
public static void RegisterModCloudPersistedSlot(string modId, string fileName, SaveScope scope)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
ModCloudSyncPathRegistry.RegisterModDataSlot(modId, fileName, scope);
}
/// <summary>
/// Registers a page in the RitsuLib mod settings submenu.
/// 在 RitsuLib Mod 设置子菜单中注册一个页面。
/// </summary>
/// <remarks>
/// Optional layout: <see cref="ModSettingsUiPresentation.ParagraphMaxBodyHeight" />.
/// 可选布局:<see cref="ModSettingsUiPresentation.ParagraphMaxBodyHeight" />。
/// </remarks>
public static void RegisterModSettings(string modId, Action<ModSettingsPageBuilder> configure,
string? pageId = null)
{
ModSettingsRegistry.Register(modId, configure, pageId);
}
/// <summary>
/// Registers a reflection-based settings provider type for attribute-driven settings pages.
/// 注册一个基于反射的设置提供器类型,用于属性驱动的设置页。
/// </summary>
public static bool RegisterModSettingsReflectionProvider<TProvider>()
{
return RuntimeReflectionMirrorSource.RegisterProviderType<TProvider>();
}
/// <summary>
/// Registers a reflection-based settings provider type for attribute-driven settings pages.
/// 注册一个基于反射的设置提供器类型,用于属性驱动的设置页。
/// </summary>
public static bool RegisterModSettingsReflectionProvider(Type providerType)
{
return RuntimeReflectionMirrorSource.RegisterProviderType(providerType);
}
/// <summary>
/// Registers a reflection provider and immediately attempts to mirror-register its pages.
/// 注册一个反射提供器,并立即尝试镜像注册其页面。
/// </summary>
public static int RegisterModSettingsReflectionProviderAndTryRegister<TProvider>()
{
return RuntimeReflectionMirrorSource.RegisterProviderTypeAndTryRegister<TProvider>();
}
/// <summary>
/// Registers a reflection provider and immediately attempts to mirror-register its pages.
/// 注册一个反射提供器,并立即尝试镜像注册其页面。
/// </summary>
public static int RegisterModSettingsReflectionProviderAndTryRegister(Type providerType)
{
return RuntimeReflectionMirrorSource.RegisterProviderTypeAndTryRegister(providerType);
}
/// <summary>
/// Sets ordering for this mod's group in the RitsuLib mod settings sidebar (lower first). Mods without a
/// value use <c>0</c> and sort by display name. Prefer <see cref="ModSettingsPageBuilder.WithModSidebarOrder" /> when
/// registering pages.
/// 设置此 mod 分组在 RitsuLib mod 设置侧边栏中的排序(较小者在前)。没有
/// 值的 mod 使用 <c>0</c> 并按显示名排序。注册页面时优先使用 <see cref="ModSettingsPageBuilder.WithModSidebarOrder" />。
/// </summary>
public static void RegisterModSettingsSidebarOrder(string modId, int order)
{
ModSettingsRegistry.RegisterModSidebarOrder(modId, order);
}
/// <summary>
/// Overrides sort order for a registered page among siblings (same mod and parent page).
/// 覆盖已注册页面在同级页面中的排序(同一 Mod 且同一父页面)。
/// </summary>
public static void RegisterModSettingsPageOrder(string modId, string pageId, int sortOrder)
{
ModSettingsRegistry.RegisterPageSortOrder(modId, pageId, sortOrder);
}
/// <summary>
/// Places <paramref name="pageId" /> after <paramref name="afterPageId" /> in the sidebar for this mod.
/// 将 <paramref name="pageId" /> 放在此 mod 侧边栏中 <paramref name="afterPageId" /> 之后。
/// </summary>
public static bool TryRegisterModSettingsPageOrderAfter(string modId, string pageId, string afterPageId,
int gap = 1)
{
return ModSettingsRegistry.TryRegisterPageSortOrderAfter(modId, pageId, afterPageId, gap);
}
/// <summary>
/// Places <paramref name="pageId" /> before <paramref name="beforePageId" /> in the sidebar for this mod.
/// 将 <paramref name="pageId" /> 放在此 mod 侧边栏中 <paramref name="beforePageId" /> 之前。
/// </summary>
public static bool TryRegisterModSettingsPageOrderBefore(string modId, string pageId, string beforePageId,
int gap = 1)
{
return ModSettingsRegistry.TryRegisterPageSortOrderBefore(modId, pageId, beforePageId, gap);
}
/// <summary>
/// Returns all registered mod settings pages (same snapshot as <see cref="ModSettingsRegistry.GetPages" />).
/// 返回所有已注册的 mod 设置页(与 <see cref="ModSettingsRegistry.GetPages" /> 相同的快照)。
/// </summary>
public static IReadOnlyList<ModSettingsPage> GetRegisteredModSettings()
{
return ModSettingsRegistry.GetPages();
}
/// <summary>
/// Creates a <c>MegaCrit.Sts2.Core.Logging.Logger</c> for <paramref name="modId" />.
/// 为 <paramref name="modId" /> 创建 <c>MegaCrit.Sts2.Core.Logging.Logger</c>。
/// </summary>
public static Logger CreateLogger(string modId, LogType logType = LogType.Generic)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
return new(modId, logType);
}
/// <summary>
/// Creates a <see cref="STS2RitsuLib.Patching.Core.ModPatcher" /> with a dedicated logger for the owning mod.
/// 使用所属 mod 的专用 logger 创建 <see cref="STS2RitsuLib.Patching.Core.ModPatcher" />。
/// </summary>
public static ModPatcher CreatePatcher(
string ownerModId,
string patcherName,
string? patcherLabel = null,
LogType logType = LogType.Generic)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ownerModId);
ArgumentException.ThrowIfNullOrWhiteSpace(patcherName);
var logger = CreateLogger(ownerModId, logType);
return new(
$"{ownerModId}.{patcherName}",
logger,
patcherLabel ?? patcherName
);
}
/// <summary>
/// Creates a <see cref="STS2RitsuLib.Utils.I18N" /> instance with optional file, embedded resource, and PCK
/// translation roots.
/// 创建 <see cref="STS2RitsuLib.Utils.I18N" /> 实例,可带可选的文件、嵌入资源和 PCK
/// 翻译根。
/// </summary>
public static I18N CreateLocalization(
string instanceName,
IEnumerable<string>? fileSystemFolders = null,
IEnumerable<string>? resourceFolders = null,
IEnumerable<string>? pckFolders = null,
Assembly? resourceAssembly = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(instanceName);
return new(
instanceName,
fileSystemFolders?.ToArray() ?? [],
resourceFolders?.ToArray() ?? [],
pckFolders?.ToArray() ?? [],
resourceAssembly ?? Assembly.GetCallingAssembly()
);
}
/// <summary>
/// Creates a <see cref="STS2RitsuLib.Utils.I18N" /> instance for a mod, defaulting the file-system folder to
/// <c>user://<platform>/<userId>/mod_data/{modId}/localization</c> when none are supplied.
/// <c>user://<platform>/<userId>/mod_data/{modId}/localization</c>。
/// 为 mod 创建 <see cref="STS2RitsuLib.Utils.I18N" /> 实例;未提供时,文件系统文件夹默认使用
/// </summary>
public static I18N CreateModLocalization(
string modId,
string instanceName,
IEnumerable<string>? fileSystemFolders = null,
IEnumerable<string>? resourceFolders = null,
IEnumerable<string>? pckFolders = null,
Assembly? resourceAssembly = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
ArgumentException.ThrowIfNullOrWhiteSpace(instanceName);
var folders = fileSystemFolders?.ToArray() ?? [$"{ProfileManager.GetAccountBasePath(modId)}/localization"];
return CreateLocalization(instanceName, folders, resourceFolders, pckFolders, resourceAssembly);
}
/// <summary>
/// Returns the virtual <c>LocTable</c> id for an <see cref="I18N" /> bridge table using the framework's
/// standard three-segment id convention: <c>MODID_I18N_STEM</c>.
/// <c>MODID_I18N_STEM</c>。
/// 返回使用框架标准三段式 id 约定的 <see cref="I18N" /> 桥接表的虚拟 <c>LocTable</c> id:
/// <c>MODID_I18N_STEM</c>。
/// <c>MODID_I18N_STEM</c>。
/// </summary>
public static string GetI18NLocTableId(string modId, string stem = "DEFAULT")
{
return I18NLocTableBridge.GetTableId(modId, stem);
}
/// <summary>
/// Registers an <see cref="I18N" /> instance as a virtual <c>LocTable</c> so the game-native
/// <c>LocString</c> pipeline can resolve raw templates from it.
/// 将 <see cref="I18N" /> 实例注册为虚拟 <c>LocTable</c>,使游戏原生
/// <c>LocString</c> 管线可以从中解析原始模板。
/// </summary>
public static bool RegisterI18NLocTableBridge(string modId, I18N i18N, string stem = "DEFAULT",
bool replaceExisting = false)
{
return I18NLocTableBridge.TryRegister(modId, i18N, stem, replaceExisting);
}
/// <summary>
/// Unregisters a previously registered virtual <c>LocTable</c> for the given <paramref name="modId" /> and
/// <paramref name="stem" />.
/// 注销此前为给定 <paramref name="modId" /> 和
/// <paramref name="stem" /> 注册的虚拟 <c>LocTable</c>。
/// </summary>
public static bool UnregisterI18NLocTableBridge(string modId, string stem = "DEFAULT")
{
return I18NLocTableBridge.TryUnregister(modId, stem);
}
/// <summary>
/// Registers C# scripts from <paramref name="assembly" /> with Godot (once per assembly).
/// 将 <paramref name="assembly" /> 中的 C# 脚本注册到 Godot(每个程序集一次)。
/// </summary>
public static void EnsureGodotScriptsRegistered(Assembly assembly, Logger? logger = null)
{
ArgumentNullException.ThrowIfNull(assembly);
var assemblyName = assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
lock (SyncRoot)
{
if (!RegisteredScriptAssemblies.Add(assemblyName))
return;
}
try
{
var bridgeType = typeof(GodotObject).Assembly.GetType("Godot.Bridge.ScriptManagerBridge");
var lookupMethod = bridgeType?.GetMethod(
"LookupScriptsInAssembly",
BindingFlags.Public | BindingFlags.Static,
null,
[typeof(Assembly)],
null);
if (lookupMethod == null)
{
logger?.Warn($"Godot script registration bridge not found for assembly {assemblyName}.");
return;
}
var lookup = lookupMethod.CreateDelegate<Action<Assembly>>();
lookup(assembly);
logger?.Debug($"Registered Godot C# scripts for assembly: {assemblyName}");
}
catch (Exception ex)
{
logger?.Error($"Failed to register Godot C# scripts for assembly {assemblyName}: {ex.Message}");
logger?.Error($"Stack trace: {ex.StackTrace}");
}
}
/// <summary>
/// Applies all patches on <paramref name="patcher" />; on failure logs, invokes <paramref name="disableMod" />, and
/// returns false.
/// 应用 <paramref name="patcher" /> 上的所有 patch;失败时记录日志,调用 <paramref name="disableMod" />,并
/// 返回 false。
/// </summary>
public static bool ApplyRequiredPatcher(ModPatcher patcher, Action disableMod, string? failureMessage = null)
{
ArgumentNullException.ThrowIfNull(patcher);
ArgumentNullException.ThrowIfNull(disableMod);
var success = patcher.PatchAll();
if (success)
return true;
patcher.Logger.Error(
failureMessage ?? $"Required patcher '{patcher.PatcherName}' failed. The mod will be disabled.");
disableMod();
return false;
}
internal static void PublishLifecycleEvent<TEvent>(TEvent evt, string phase)
where TEvent : IFrameworkLifecycleEvent
{
var typedHandlers = Array.Empty<Action<TEvent>>();
ILifecycleObserver[] observers;
lock (SyncRoot)
{
if (LifecycleEventTypeCache<TEvent>.InvalidatesProfileDataReady)
ReplayableLifecycleEvents.Remove(typeof(ProfileDataReadyEvent));
if (LifecycleEventTypeCache<TEvent>.IsReplayable)
ReplayableLifecycleEvents[LifecycleEventTypeCache<TEvent>.EventType] = evt;
observers = _lifecycleObservers;
}
if (LifecycleEventTypeCache<TEvent>.SupportsTypedDispatch)
typedHandlers = GetLifecycleTopic<TEvent>().ReadSnapshot();
foreach (var handler in typedHandlers)
SafeNotify(handler, evt, phase);
foreach (var observer in observers)
SafeNotify(observer, evt, phase);
}
private static T[] AppendItem<T>(T[] source, T item)
{
var result = new T[source.Length + 1];