diff --git a/Common/ModSystems/ModIntegrationsSystem.cs b/Common/ModSystems/ModIntegrationsSystem.cs
index c3acdc98..20c66ff0 100644
--- a/Common/ModSystems/ModIntegrationsSystem.cs
+++ b/Common/ModSystems/ModIntegrationsSystem.cs
@@ -759,6 +759,30 @@ public static object Call(params object[] args)
}
return false;
}
+ // 注册一个气候控制项
+ case "RegisterWeatherControl":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.RegisterControl(args);
+ // 移除一个气候控制项
+ case "UnregisterWeatherControl":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.UnregisterControl(args);
+ // 注册一个自定义内容栏
+ case "RegisterWeatherControlGroup":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.RegisterGroup(args);
+ // 移除一个自定义内容栏
+ case "UnregisterWeatherControlGroup":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.UnregisterGroup(args);
+ // 查询某控制项的当前档位,未知返回 -1
+ case "QueryWeatherControlStage":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.QueryStage(args[1] as string);
+ // 派发档位变更
+ case "SetWeatherControlStage":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.DispatchStage(args[1] as string, Convert.ToInt32(args[2]));
+ // 查询某控制项是否锁定
+ case "IsWeatherControlLocked":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.QueryLocked(args[1] as string);
+ // 派发锁定状态变更
+ case "SetWeatherControlLocked":
+ return Content.Functions.WeatherControl.WeatherControlCrossMod.DispatchLocked(args[1] as string, Convert.ToBoolean(args[2]));
default:
ImproveGame.Instance.Logger.Error($"Replacement type \"{msg}\" not found.");
return false;
diff --git a/Content/Functions/WeatherControl/IWeatherControl.cs b/Content/Functions/WeatherControl/IWeatherControl.cs
new file mode 100644
index 00000000..b852f352
--- /dev/null
+++ b/Content/Functions/WeatherControl/IWeatherControl.cs
@@ -0,0 +1,63 @@
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 一个气候控制项的抽象,内置项与外部模组项都实现这个接口
+///
+public interface IWeatherControl
+{
+ /// 注册源 Mod 的内部名
+ string ModName { get; }
+
+ /// 项目的内部名
+ string Name { get; }
+
+ /// 唯一标识,等于 ModName 加冒号加 Name
+ string Id => $"{ModName}:{Name}";
+
+ /// 槽位图标
+ Texture2D Icon { get; }
+
+ ///
+ /// 所属内容栏的 Id,为空则归到默认的模组项网格
+ ///
+ string GroupId { get; }
+
+ /// 排序优先级,越高越靠前
+ int Priority { get; }
+
+ ///
+ /// 档位名数组,至少包含两个
+ /// 名字是模组内部用的英文键,不直接显示
+ ///
+ IReadOnlyList Stages { get; }
+
+ /// 是否支持锁定
+ bool SupportsLock { get; }
+
+ /// 当前是否可用,不可用时 UI 灰显或隐藏
+ bool IsAvailable { get; }
+
+ /// 显示名
+ string GetDisplayName();
+
+ /// 悬停说明,可返回 null
+ string GetTooltip();
+
+ /// 取当前档位下标,未知返回 -1
+ int GetStage();
+
+ ///
+ /// 应用一个档位到本地
+ /// 这一层不处理网络同步,网络同步在 的派发层完成
+ ///
+ void SetStage(int stage);
+
+ /// 取当前是否锁定
+ bool GetLocked();
+
+ ///
+ /// 应用锁定状态到本地
+ /// 这一层不处理网络同步
+ ///
+ void SetLocked(bool locked);
+}
diff --git a/Content/Functions/WeatherControl/IWeatherControlGroup.cs b/Content/Functions/WeatherControl/IWeatherControlGroup.cs
new file mode 100644
index 00000000..08e7b1bf
--- /dev/null
+++ b/Content/Functions/WeatherControl/IWeatherControlGroup.cs
@@ -0,0 +1,32 @@
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 气候控制面板里的一整栏内容
+/// 内置艺术画面与模组项网格各是一栏,外部 Mod 可以注册自己的栏
+///
+public interface IWeatherControlGroup
+{
+ /// 注册源 Mod 的内部名
+ string ModName { get; }
+
+ /// 内容栏的内部名
+ string Name { get; }
+
+ /// 唯一标识
+ string Id => $"{ModName}:{Name}";
+
+ /// 排序优先级,越高越靠前
+ int Priority { get; }
+
+ /// 该栏当前是否可用
+ bool IsAvailable { get; }
+
+ /// 显示名
+ string GetDisplayName();
+
+ ///
+ /// 创建该栏的视图,每次 WeatherGUI 打开时调用一次
+ /// 返回的 UIElement 会作为子元素挂到主面板下
+ ///
+ UIElement CreateView(IReadOnlyList controls);
+}
diff --git a/Content/Functions/WeatherControl/WeatherBuiltinControls.cs b/Content/Functions/WeatherControl/WeatherBuiltinControls.cs
new file mode 100644
index 00000000..dba83e9a
--- /dev/null
+++ b/Content/Functions/WeatherControl/WeatherBuiltinControls.cs
@@ -0,0 +1,169 @@
+using Terraria.GameContent.Events;
+
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 把内置 5 项注册进 ,让外部 Mod 也能查询和操作它们
+///
+internal static class WeatherBuiltinControls
+{
+ public const string TimeId = "Time";
+ public const string MoonPhaseId = "MoonPhase";
+ public const string RainId = "Rain";
+ public const string SandstormId = "Sandstorm";
+ public const string WindId = "Wind";
+
+ public static string FullId(string name) => $"{WeatherControlRegistry.BuiltinModName}:{name}";
+
+ public static void RegisterAll(WeatherControlRegistry registry)
+ {
+ string groupId = WeatherControlRegistry.BuiltinGroupId;
+
+ registry.Register(new WeatherControlInfo
+ {
+ ModName = WeatherControlRegistry.BuiltinModName,
+ Name = TimeId,
+ GroupId = groupId,
+ Priority = 50,
+ Icon = ModAsset.ClockHighlight.Value,
+ Stages = ["Dawn", "Noon", "Dusk", "Midnight"],
+ SupportsLock = false,
+ DisplayNameProvider = () => GetText("UI.WeatherGUI.Time"),
+ AvailableProvider = () => Main.hardMode,
+ StageProvider = () => -1,
+ StageSetter = ApplyTime,
+ });
+
+ registry.Register(new WeatherControlInfo
+ {
+ ModName = WeatherControlRegistry.BuiltinModName,
+ Name = MoonPhaseId,
+ GroupId = groupId,
+ Priority = 40,
+ Icon = ModAsset.MoonPhaseHighlight.Value,
+ Stages = ["Phase0", "Phase1", "Phase2", "Phase3", "Phase4", "Phase5", "Phase6", "Phase7"],
+ SupportsLock = true,
+ DisplayNameProvider = () => GetText("UI.WeatherGUI.MoonPhase"),
+ StageProvider = () => Main.moonPhase,
+ StageSetter = ApplyMoonPhase,
+ LockedProvider = () => WeatherController.MoonPhaseLocked,
+ LockedSetter = v => WeatherController.MoonPhaseLocked = v,
+ });
+
+ registry.Register(new WeatherControlInfo
+ {
+ ModName = WeatherControlRegistry.BuiltinModName,
+ Name = RainId,
+ GroupId = groupId,
+ Priority = 30,
+ Icon = ModAsset.RainActive.Value,
+ Stages = ["Off", "On"],
+ SupportsLock = true,
+ DisplayNameProvider = () => GetText("UI.WeatherGUI." + (Main.raining ? "RainInactive" : "RainActive")),
+ StageProvider = () => Main.raining ? 1 : 0,
+ StageSetter = ApplyRain,
+ LockedProvider = () => WeatherController.RainLocked,
+ LockedSetter = v => WeatherController.RainLocked = v,
+ });
+
+ registry.Register(new WeatherControlInfo
+ {
+ ModName = WeatherControlRegistry.BuiltinModName,
+ Name = SandstormId,
+ GroupId = groupId,
+ Priority = 20,
+ Icon = ModAsset.SandstormActive.Value,
+ Stages = ["Off", "On"],
+ SupportsLock = true,
+ DisplayNameProvider = () => GetText("UI.WeatherGUI." + (Sandstorm.Happening ? "SandstormInactive" : "SandstormActive")),
+ StageProvider = () => Sandstorm.Happening ? 1 : 0,
+ StageSetter = ApplySandstorm,
+ LockedProvider = () => WeatherController.SandstormLocked,
+ LockedSetter = v => WeatherController.SandstormLocked = v,
+ });
+
+ registry.Register(new WeatherControlInfo
+ {
+ ModName = WeatherControlRegistry.BuiltinModName,
+ Name = WindId,
+ GroupId = groupId,
+ Priority = 10,
+ Icon = ModAsset.Wheel.Value,
+ Stages = ["West", "No", "East"],
+ SupportsLock = true,
+ DisplayNameProvider = () => GetText("UI.WeatherGUI.Wind"),
+ StageProvider = GetWindStage,
+ StageSetter = ApplyWind,
+ LockedProvider = () => WeatherController.WindLocked,
+ LockedSetter = v => WeatherController.WindLocked = v,
+ });
+ }
+
+ private static void ApplyTime(int stage)
+ {
+ switch (stage)
+ {
+ case 0: Main.SkipToTime(0, setIsDayTime: true); break;
+ case 1: Main.SkipToTime(27000, setIsDayTime: true); break;
+ case 2: Main.SkipToTime(0, setIsDayTime: false); break;
+ case 3: Main.SkipToTime(16200, setIsDayTime: false); break;
+ }
+ }
+
+ private static void ApplyMoonPhase(int stage)
+ {
+ if (stage < 0) stage = 0;
+ Main.moonPhase = stage % 8;
+ }
+
+ private static void ApplyRain(int stage)
+ {
+ bool target = stage != 0;
+ if (target == Main.raining) return;
+
+ if (target)
+ {
+ Main.StartRain();
+ Main.cloudAlpha = 0.7f;
+ Main.maxRaining = 0.7f;
+ }
+ else
+ {
+ Main.StopRain();
+ Main.cloudAlpha = 0f;
+ Main.maxRaining = 0f;
+ }
+ }
+
+ private static void ApplySandstorm(int stage)
+ {
+ bool target = stage != 0;
+ if (target == Sandstorm.Happening) return;
+
+ if (target)
+ Sandstorm.StartSandstorm();
+ else
+ Sandstorm.StopSandstorm();
+ }
+
+ private static int GetWindStage()
+ {
+ // 与原版 SetWindPacket 的 stage 数值对齐
+ // 0=West, 1=No, 2=East 三档,无风段按绝对值阈值划分
+ float w = Main.windSpeedCurrent;
+ if (w >= 0.4f) return 0;
+ if (w <= -0.4f) return 2;
+ return 1;
+ }
+
+ private static void ApplyWind(int stage)
+ {
+ float v = stage switch
+ {
+ 0 => 0.61f,
+ 2 => -0.61f,
+ _ => Main.rand.NextFloat(-0.04f, 0.04f)
+ };
+ Main.windSpeedCurrent = Main.windSpeedTarget = v;
+ }
+}
diff --git a/Content/Functions/WeatherControl/WeatherControlCrossMod.cs b/Content/Functions/WeatherControl/WeatherControlCrossMod.cs
new file mode 100644
index 00000000..32bf9a98
--- /dev/null
+++ b/Content/Functions/WeatherControl/WeatherControlCrossMod.cs
@@ -0,0 +1,154 @@
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 把 Mod.Call 收到的弱引用参数转成 注册进 Registry
+///
+internal static class WeatherControlCrossMod
+{
+ public static bool RegisterControl(object[] args)
+ {
+ int len = args.Length;
+ if (len < 9) return false;
+
+ var sourceMod = args[1] as Mod;
+ if (sourceMod is null) return false;
+
+ var name = args[2] as string;
+ if (string.IsNullOrEmpty(name)) return false;
+
+ var icon = args[3] as Texture2D;
+ var displayName = ToStringFunc(args[4]);
+ var tooltip = ToStringFunc(args[5]);
+
+ var stages = args[6] as IReadOnlyList ?? (args[6] is string[] arr ? arr : null);
+ if (stages is null || stages.Count < 2) return false;
+
+ var stageProvider = args[7] as Func;
+ var stageSetter = args[8] as Action;
+
+ bool supportsLock = len > 9 && args[9] is bool b9 && b9;
+ var lockedProvider = len > 10 ? args[10] as Func : null;
+ var lockedSetter = len > 11 ? args[11] as Action : null;
+ var availableProvider = len > 12 ? args[12] as Func : null;
+ int priority = len > 13 && args[13] is int p ? p : 0;
+ var groupId = len > 14 ? args[14] as string : null;
+
+ var info = new WeatherControlInfo
+ {
+ ModName = sourceMod.Name,
+ Name = name,
+ GroupId = groupId,
+ Priority = priority,
+ Icon = icon,
+ Stages = stages,
+ SupportsLock = supportsLock,
+ DisplayNameProvider = displayName,
+ TooltipProvider = tooltip,
+ AvailableProvider = availableProvider,
+ StageProvider = stageProvider,
+ StageSetter = stageSetter,
+ LockedProvider = lockedProvider,
+ LockedSetter = lockedSetter,
+ };
+
+ return WeatherControlRegistry.Instance.Register(info);
+ }
+
+ public static bool UnregisterControl(object[] args)
+ {
+ if (args.Length < 3) return false;
+ var sourceMod = args[1] as Mod;
+ var name = args[2] as string;
+ if (sourceMod is null || string.IsNullOrEmpty(name)) return false;
+ return WeatherControlRegistry.Instance.Unregister(sourceMod.Name, name);
+ }
+
+ public static bool RegisterGroup(object[] args)
+ {
+ if (args.Length < 6) return false;
+ var sourceMod = args[1] as Mod;
+ var name = args[2] as string;
+ var displayName = ToStringFunc(args[3]);
+ if (sourceMod is null || string.IsNullOrEmpty(name)) return false;
+ int priority = args[4] is int p ? p : 0;
+ var createContent = args[5] as Func;
+ if (createContent is null) return false;
+ var availableProvider = args.Length > 6 ? args[6] as Func : null;
+
+ var group = new CrossModWeatherControlGroup
+ {
+ ModName = sourceMod.Name,
+ Name = name,
+ Priority = priority,
+ DisplayNameProvider = displayName,
+ AvailableProvider = availableProvider,
+ ContentProvider = createContent,
+ };
+
+ return WeatherControlRegistry.Instance.RegisterGroup(group);
+ }
+
+ public static bool UnregisterGroup(object[] args)
+ {
+ if (args.Length < 3) return false;
+ var sourceMod = args[1] as Mod;
+ var name = args[2] as string;
+ if (sourceMod is null || string.IsNullOrEmpty(name)) return false;
+ return WeatherControlRegistry.Instance.UnregisterGroup(sourceMod.Name, name);
+ }
+
+ public static int QueryStage(string id)
+ => WeatherControlRegistry.Instance.TryGet(id, out var c) ? c.GetStage() : -1;
+
+ public static bool DispatchStage(string id, int stage)
+ {
+ if (string.IsNullOrEmpty(id)) return false;
+ if (!WeatherControlRegistry.Instance.TryGet(id, out _)) return false;
+ WeatherControlRegistry.Instance.DispatchStage(id, stage);
+ return true;
+ }
+
+ public static bool QueryLocked(string id)
+ => WeatherControlRegistry.Instance.TryGet(id, out var c) && c.GetLocked();
+
+ public static bool DispatchLocked(string id, bool locked)
+ {
+ if (string.IsNullOrEmpty(id)) return false;
+ if (!WeatherControlRegistry.Instance.TryGet(id, out var c)) return false;
+ if (!c.SupportsLock) return false;
+ WeatherControlRegistry.Instance.DispatchLocked(id, locked);
+ return true;
+ }
+
+ private static Func ToStringFunc(object arg)
+ {
+ return arg switch
+ {
+ null => null,
+ Func f => f,
+ LocalizedText lt => () => lt.Value,
+ string s => () => s,
+ _ => null
+ };
+ }
+}
+
+///
+/// Mod.Call 注册自定义内容栏用的实现
+///
+internal sealed class CrossModWeatherControlGroup : IWeatherControlGroup
+{
+ public string ModName { get; init; }
+ public string Name { get; init; }
+ public int Priority { get; init; }
+ public Func DisplayNameProvider { get; init; }
+ public Func AvailableProvider { get; init; }
+ public Func ContentProvider { get; init; }
+
+ public bool IsAvailable => AvailableProvider is null || AvailableProvider();
+
+ public string GetDisplayName() => DisplayNameProvider?.Invoke() ?? Name;
+
+ public UIElement CreateView(IReadOnlyList controls)
+ => ContentProvider?.Invoke();
+}
diff --git a/Content/Functions/WeatherControl/WeatherControlInfo.cs b/Content/Functions/WeatherControl/WeatherControlInfo.cs
new file mode 100644
index 00000000..75e0fc3a
--- /dev/null
+++ b/Content/Functions/WeatherControl/WeatherControlInfo.cs
@@ -0,0 +1,40 @@
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 普通气候控制项的具体实现,内置项与 Mod.Call 注册的项都用这个
+///
+public sealed class WeatherControlInfo : IWeatherControl
+{
+ public string ModName { get; init; }
+ public string Name { get; init; }
+ public string GroupId { get; init; }
+ public int Priority { get; init; }
+ public Texture2D Icon { get; init; }
+
+ public IReadOnlyList Stages { get; init; } = ["Off", "On"];
+ public bool SupportsLock { get; init; }
+
+ public Func DisplayNameProvider { get; init; }
+ public Func TooltipProvider { get; init; }
+ public Func AvailableProvider { get; init; }
+ public Func StageProvider { get; init; }
+ public Action StageSetter { get; init; }
+ public Func LockedProvider { get; init; }
+ public Action LockedSetter { get; init; }
+
+ public bool IsAvailable => AvailableProvider is null || AvailableProvider();
+
+ public string GetDisplayName() => DisplayNameProvider?.Invoke() ?? Name;
+ public string GetTooltip() => TooltipProvider?.Invoke();
+
+ public int GetStage() => StageProvider?.Invoke() ?? -1;
+ public void SetStage(int stage) => StageSetter?.Invoke(stage);
+
+ public bool GetLocked() => SupportsLock && (LockedProvider?.Invoke() ?? false);
+
+ public void SetLocked(bool locked)
+ {
+ if (!SupportsLock) return;
+ LockedSetter?.Invoke(locked);
+ }
+}
diff --git a/Content/Functions/WeatherControl/WeatherControlRegistry.cs b/Content/Functions/WeatherControl/WeatherControlRegistry.cs
new file mode 100644
index 00000000..09124dfc
--- /dev/null
+++ b/Content/Functions/WeatherControl/WeatherControlRegistry.cs
@@ -0,0 +1,142 @@
+using ImproveGame.Packets.Weather;
+
+namespace ImproveGame.Content.Functions.WeatherControl;
+
+///
+/// 气候控制注册中心,内置项与外部 Mod 项都在这里登记
+///
+public sealed class WeatherControlRegistry : ModSystem
+{
+ /// 内置项使用的 ModName
+ public const string BuiltinModName = "ImproveGame";
+
+ /// 内置艺术画面栏的 Name
+ public const string BuiltinGroupName = "Builtin";
+
+ /// 模组项默认网格栏的 Name
+ public const string ModdedGroupName = "Modded";
+
+ /// 内置艺术画面栏的完整 Id
+ public static string BuiltinGroupId => $"{BuiltinModName}:{BuiltinGroupName}";
+
+ /// 模组项默认网格栏的完整 Id
+ public static string ModdedGroupId => $"{BuiltinModName}:{ModdedGroupName}";
+
+ public static WeatherControlRegistry Instance => ModContent.GetInstance();
+
+ private readonly Dictionary _controls = [];
+ private readonly Dictionary _groups = [];
+
+ ///
+ /// 注册一个控制项,重名直接覆盖
+ ///
+ public bool Register(IWeatherControl control)
+ {
+ if (control is null) return false;
+ if (string.IsNullOrEmpty(control.ModName) || string.IsNullOrEmpty(control.Name)) return false;
+ if (control.Stages is null || control.Stages.Count < 2) return false;
+ _controls[control.Id] = control;
+ return true;
+ }
+
+ public bool Unregister(string modName, string name)
+ {
+ if (string.IsNullOrEmpty(modName) || string.IsNullOrEmpty(name)) return false;
+ return _controls.Remove($"{modName}:{name}");
+ }
+
+ public int UnregisterAllOf(string modName)
+ {
+ if (string.IsNullOrEmpty(modName)) return 0;
+ int count = 0;
+ foreach (var id in _controls.Where(p => p.Value.ModName == modName).Select(p => p.Key).ToList())
+ {
+ _controls.Remove(id);
+ count++;
+ }
+ foreach (var id in _groups.Where(p => p.Value.ModName == modName).Select(p => p.Key).ToList())
+ {
+ _groups.Remove(id);
+ count++;
+ }
+ return count;
+ }
+
+ public bool TryGet(string id, out IWeatherControl control) => _controls.TryGetValue(id, out control);
+
+ public IWeatherControl Get(string id) => _controls.TryGetValue(id, out var c) ? c : null;
+
+ public IReadOnlyCollection AllControls => _controls.Values;
+
+ public bool RegisterGroup(IWeatherControlGroup group)
+ {
+ if (group is null) return false;
+ if (string.IsNullOrEmpty(group.ModName) || string.IsNullOrEmpty(group.Name)) return false;
+ _groups[group.Id] = group;
+ return true;
+ }
+
+ public bool UnregisterGroup(string modName, string name)
+ {
+ if (string.IsNullOrEmpty(modName) || string.IsNullOrEmpty(name)) return false;
+ return _groups.Remove($"{modName}:{name}");
+ }
+
+ /// 已注册的所有内容栏,按优先级倒序
+ public IEnumerable SortedGroups() => _groups.Values.OrderByDescending(g => g.Priority);
+
+ ///
+ /// 默认模组项网格栏要展示的控制项
+ /// 即未指定 GroupId、或显式指定到模组项网格栏的项
+ ///
+ public IEnumerable ModdedGridControls()
+ {
+ string moddedId = ModdedGroupId;
+ return _controls.Values
+ .Where(c => string.IsNullOrEmpty(c.GroupId) || c.GroupId == moddedId)
+ .OrderByDescending(c => c.Priority);
+ }
+
+ /// 指定栏下的控制项
+ public IEnumerable ControlsInGroup(string groupId)
+ {
+ return _controls.Values
+ .Where(c => c.GroupId == groupId)
+ .OrderByDescending(c => c.Priority);
+ }
+
+ ///
+ /// 发送档位变更,所有客户端会同步应用
+ /// 单机会原地应用一次
+ ///
+ public void DispatchStage(string id, int stage)
+ {
+ if (!_controls.ContainsKey(id)) return;
+ WeatherStageSyncPacket.Send(id, stage);
+ }
+
+ ///
+ /// 发送锁定状态变更
+ ///
+ public void DispatchLocked(string id, bool locked)
+ {
+ if (!TryGet(id, out var c)) return;
+ if (!c.SupportsLock) return;
+ WeatherLockSyncPacket.Send(id, locked);
+ }
+
+ public override void Load()
+ {
+ }
+
+ public override void PostSetupContent()
+ {
+ WeatherBuiltinControls.RegisterAll(this);
+ }
+
+ public override void Unload()
+ {
+ _controls.Clear();
+ _groups.Clear();
+ }
+}
diff --git a/Content/Functions/WeatherController.cs b/Content/Functions/WeatherController.cs
index c073743e..d8bc2f35 100644
--- a/Content/Functions/WeatherController.cs
+++ b/Content/Functions/WeatherController.cs
@@ -129,7 +129,7 @@ public override void SaveWorldData(TagCompound tag)
{
if (Unlocked) tag.Add("unlocked", true);
if (RainLocked) tag.Add("rainLocked", true);
- if (SandstormLocked) tag.Add("sandstorm", true);
+ if (SandstormLocked) tag.Add("sandstormLocked", true);
if (MoonPhaseLocked) tag.Add("moonPhaseLocked", true);
if (WindLocked) tag.Add("windLocked", true);
}
@@ -138,7 +138,9 @@ public override void LoadWorldData(TagCompound tag)
{
if (tag.ContainsKey("unlocked")) Unlocked = tag.GetBool("unlocked");
if (tag.ContainsKey("rainLocked")) RainLocked = tag.GetBool("rainLocked");
+ // 旧版本写入的键名是 sandstorm,这里兼容一下
if (tag.ContainsKey("sandstormLocked")) SandstormLocked = tag.GetBool("sandstormLocked");
+ else if (tag.ContainsKey("sandstorm")) SandstormLocked = tag.GetBool("sandstorm");
if (tag.ContainsKey("moonPhaseLocked")) MoonPhaseLocked = tag.GetBool("moonPhaseLocked");
if (tag.ContainsKey("windLocked")) WindLocked = tag.GetBool("windLocked");
}
diff --git a/CrossModSupport.md b/CrossModSupport.md
index 88fa5eb0..62f36caf 100644
--- a/CrossModSupport.md
+++ b/CrossModSupport.md
@@ -388,4 +388,165 @@ public class MyMod : Mod
//这里可以偷懒不卸载委托,因为模组在重新加载时本身会自动卸载清理所有的委托
}
+```
+
+### RegisterWeatherControl
+
+往气候控制面板注册一个控制项,未指定栏目的项会落到面板里的“模组项”栏
+建议把本项目根目录的 [ImproveGame_WeatherControlCrossModHelper.cs](ImproveGame_WeatherControlCrossModHelper.cs) 复制到你自己的模组里,直接用强类型 API 调用,省去手动凑 `Mod.Call` 参数
+
+#### 参数
+
+- `Mod` 注册源模组的实例,会被用作控制项的 ModName
+- `string` 控制项的内部名,与 ModName 共同构成唯一标识 `ModName:Name`
+- `Texture2D` 槽位图标,建议 32x32 以内,超过会自动按比例缩到 28x28
+- `Func/LocalizedText/string` 显示名提供器,悬停 tooltip 的顶行
+- `Func/LocalizedText/string` 悬停说明,可传 `null`
+- `string[]/IReadOnlyList` 档位名数组,长度至少 2,索引顺序就是档位顺序,名字仅用作内部键不会直接显示
+- `Func` 取当前档位下标的回调,未知时返回 `-1`
+- `Action` 应用指定档位到本地的回调,只负责落地状态不负责发包
+- `bool` 可选,是否支持锁定,默认 `false`
+- `Func` 可选,取当前锁定状态的回调
+- `Action` 可选,应用锁定状态到本地的回调
+- `Func` 可选,判断该项当前是否可用,不可用时槽位不会出现在面板上
+- `int` 可选,排序优先级,越高越靠前,默认 `0`
+- `string` 可选,所属内容栏的 Id,为空则放进默认的“模组项”栏
+
+#### 返回值
+
+- `bool` 注册是否成功,stages 数量不足或必填项缺失时返回 `false`
+
+#### 说明
+
+- `setStage` 与 `setLocked` 只在本地写入状态,跨客户端同步由 `SetWeatherControlStage` / `SetWeatherControlLocked` 统一派发,单机也走派发
+- 同模组同名重复注册会覆盖旧条目
+
+### UnregisterWeatherControl
+
+移除一个气候控制项
+
+#### 参数
+
+- `Mod` 注册源模组的实例
+- `string` 控制项的内部名
+
+### RegisterWeatherControlGroup
+
+注册一个自定义内容栏,模组可以把自己的 UI 视图挂到气候控制面板里,而不是只往“模组项”格子里塞图标
+
+#### 参数
+
+- `Mod` 注册源模组的实例
+- `string` 内容栏的内部名
+- `Func/LocalizedText/string` 显示名
+- `int` 排序优先级,越高越靠前
+- `Func` 创建该栏视图的回调,气候控制面板每次打开会调一次
+- `Func` 可选,判断该栏当前是否可用
+
+### UnregisterWeatherControlGroup
+
+移除一个自定义内容栏
+
+#### 参数
+
+- `Mod` 注册源模组的实例
+- `string` 内容栏的内部名
+
+### QueryWeatherControlStage
+
+查询某个控制项的当前档位下标,可以用来读取内置项的状态
+
+#### 参数
+
+- `string` 控制项的完整 Id,格式 `ModName:Name`
+
+#### 返回值
+
+- `int` 当前档位下标,控制项不存在时返回 `-1`
+
+### SetWeatherControlStage
+
+派发档位变更,所有客户端会同步应用
+
+#### 参数
+
+- `string` 控制项的完整 Id
+- `int` 目标档位下标
+
+### IsWeatherControlLocked
+
+查询某个控制项是否处于锁定状态
+
+#### 参数
+
+- `string` 控制项的完整 Id
+
+#### 返回值
+
+- `bool` 当前是否锁定,控制项不存在或不支持锁定时返回 `false`
+
+### SetWeatherControlLocked
+
+派发锁定状态变更,所有客户端会同步应用,控制项不支持锁定时返回 `false`
+
+#### 参数
+
+- `string` 控制项的完整 Id
+- `bool` 目标锁定状态
+
+### 内置气候控制项 Id 速查
+
+| Id | 项目 | 档位 |
+| --- | --- | --- |
+| `ImproveGame:Time` | 时间 | `Dawn` / `Noon` / `Dusk` / `Midnight` |
+| `ImproveGame:MoonPhase` | 月相 | `Phase0` ~ `Phase7` |
+| `ImproveGame:Rain` | 降雨 | `Off` / `On` |
+| `ImproveGame:Sandstorm` | 沙暴 | `Off` / `On` |
+| `ImproveGame:Wind` | 风向 | `West` / `No` / `East` |
+
+### 使用例
+
+下面这段演示一个外部模组怎么用 `Mod.Call`(这里用了 [ImproveGame_WeatherControlCrossModHelper.cs](ImproveGame_WeatherControlCrossModHelper.cs) 提供的强类型封装)注册一个自己的气候控制项:
+
+```CSharp
+public override void PostSetupContent()
+{
+ if (!ModLoader.TryGetMod("ImproveGame", out var qot)) return;
+
+ ImproveGame_WeatherControlCrossModHelper.RegisterWeatherControl(
+ qot: qot,
+ source: this,
+ name: "ScorchingDay",
+ icon: TextureAssets.Item[ItemID.LivingFireBlock].Value,
+ displayName: () => Language.GetTextValue("Mods.MyMod.ScorchingDay.Name"),
+ tooltip: () => Language.GetTextValue("Mods.MyMod.ScorchingDay.Tooltip"),
+ stages: ["Inactive", "Active"],
+ getStage: () => MySystem.Active ? 1 : 0,
+ setStage: MySystem.ApplyStage,
+ supportsLock: true,
+ getLocked: () => MySystem.Locked,
+ setLocked: locked => MySystem.Locked = locked,
+ priority: 100);
+}
+
+public override void Unload()
+{
+ // 卸载时撤掉注册,免得 Registry 留着指向已经卸载程序集的回调
+ if (ModLoader.TryGetMod("ImproveGame", out var qot))
+ ImproveGame_WeatherControlCrossModHelper.UnregisterWeatherControl(qot, this, "ScorchingDay");
+}
+```
+
+档位切换通过 `SetWeatherControlStage` 统一派发,下面这段是改造原版降雨、再读回当前档位的最小演示:
+
+```CSharp
+if (ModLoader.TryGetMod("ImproveGame", out var qot))
+{
+ // 让世界开始下雨
+ ImproveGame_WeatherControlCrossModHelper.SetWeatherControlStage(
+ qot, ImproveGame_WeatherControlCrossModHelper.RainId, 1);
+
+ int now = ImproveGame_WeatherControlCrossModHelper.QueryWeatherControlStage(
+ qot, ImproveGame_WeatherControlCrossModHelper.RainId);
+}
```
\ No newline at end of file
diff --git a/ImproveGame_WeatherControlCrossModHelper.cs b/ImproveGame_WeatherControlCrossModHelper.cs
new file mode 100644
index 00000000..ec243a11
--- /dev/null
+++ b/ImproveGame_WeatherControlCrossModHelper.cs
@@ -0,0 +1,81 @@
+using Microsoft.Xna.Framework.Graphics;
+using Terraria.Localization;
+using Terraria.ModLoader;
+using Terraria.UI;
+
+namespace ImproveGame;
+// 复制到你的项目中之后记得右键解决方案资源管理器中的项目然后同步命名空间
+// Copy it to your project and sync the namespace
+
+// 这个文件提供 ImproveGame 气候控制面板的跨模组注册支持
+// This file provides cross-mod assistance for registering items in the Weather Control panel
+public static class ImproveGame_WeatherControlCrossModHelper
+{
+ // 内置项的 Id 速查,便于跨模组调用时操作内置控制
+ // Quick reference for the built-in control ids
+ public const string BuiltinModName = "ImproveGame";
+ public const string TimeId = "ImproveGame:Time";
+ public const string MoonPhaseId = "ImproveGame:MoonPhase";
+ public const string RainId = "ImproveGame:Rain";
+ public const string SandstormId = "ImproveGame:Sandstorm";
+ public const string WindId = "ImproveGame:Wind";
+
+ ///
+ /// 注册一个气候控制项 | Register a weather control item
+ ///
stages 至少两个,索引顺序就是档位顺序
+ ///
stages must contain at least 2 entries
+ ///
+ public static bool RegisterWeatherControl(Mod qot, Mod source, string name, Texture2D icon,
+ Func displayName, Func tooltip,
+ string[] stages, Func getStage, Action setStage,
+ bool supportsLock = false, Func getLocked = null, Action setLocked = null,
+ Func isAvailable = null, int priority = 0, string groupId = null)
+ => (bool)qot.Call(nameof(RegisterWeatherControl), source, name, icon,
+ displayName, tooltip, stages, getStage, setStage,
+ supportsLock, getLocked, setLocked, isAvailable, priority, groupId);
+
+ ///
+ /// 移除一个气候控制项 | Unregister a weather control item
+ ///
+ public static bool UnregisterWeatherControl(Mod qot, Mod source, string name)
+ => (bool)qot.Call(nameof(UnregisterWeatherControl), source, name);
+
+ ///
+ /// 注册一个自定义内容栏 | Register a custom content section
+ ///
+ public static bool RegisterWeatherControlGroup(Mod qot, Mod source, string name,
+ Func displayName, int priority, Func createContent, Func isAvailable = null)
+ => (bool)qot.Call(nameof(RegisterWeatherControlGroup), source, name, displayName, priority, createContent, isAvailable);
+
+ ///
+ /// 移除一个自定义内容栏 | Unregister a custom content section
+ ///
+ public static bool UnregisterWeatherControlGroup(Mod qot, Mod source, string name)
+ => (bool)qot.Call(nameof(UnregisterWeatherControlGroup), source, name);
+
+ ///
+ /// 查询某控制项的当前档位,未知返回 -1
+ /// Query the current stage of a control, returns -1 if unknown
+ ///
+ public static int QueryWeatherControlStage(Mod qot, string id)
+ => (int)qot.Call(nameof(QueryWeatherControlStage), id);
+
+ ///
+ /// 派发档位变更,所有客户端会同步应用
+ /// Dispatch a stage change, all clients apply the update
+ ///
+ public static bool SetWeatherControlStage(Mod qot, string id, int stage)
+ => (bool)qot.Call(nameof(SetWeatherControlStage), id, stage);
+
+ ///
+ /// 查询某控制项是否锁定 | Query whether a control is currently locked
+ ///
+ public static bool IsWeatherControlLocked(Mod qot, string id)
+ => (bool)qot.Call(nameof(IsWeatherControlLocked), id);
+
+ ///
+ /// 派发锁定状态变更 | Dispatch a locked state change
+ ///
+ public static bool SetWeatherControlLocked(Mod qot, string id, bool locked)
+ => (bool)qot.Call(nameof(SetWeatherControlLocked), id, locked);
+}
diff --git a/Localization/UI/en-US_Mods.ImproveGame.UI.hjson b/Localization/UI/en-US_Mods.ImproveGame.UI.hjson
index d773671a..73829288 100644
--- a/Localization/UI/en-US_Mods.ImproveGame.UI.hjson
+++ b/Localization/UI/en-US_Mods.ImproveGame.UI.hjson
@@ -108,6 +108,8 @@ WeatherGUI: {
EasterEgg: Mushroom!
Locked: Use [i:ImproveGame/WeatherBook] to unlock weather control
TimeUnlockCondition: Defeat the Wall of Flesh to control time
+ ModdedSlotLocked: Locked, to unlock
+ ModdedSlotUnlocked: to lock the current state
}
PlayerStats: {
diff --git a/Localization/UI/zh-Hans_Mods.ImproveGame.UI.hjson b/Localization/UI/zh-Hans_Mods.ImproveGame.UI.hjson
index 2c7a8b36..fe53b617 100644
--- a/Localization/UI/zh-Hans_Mods.ImproveGame.UI.hjson
+++ b/Localization/UI/zh-Hans_Mods.ImproveGame.UI.hjson
@@ -108,6 +108,8 @@ WeatherGUI: {
EasterEgg: 蘑菇!
Locked: 使用 [i:ImproveGame/WeatherBook] 以解锁气候控制
TimeUnlockCondition: 肉后解锁时间控制
+ ModdedSlotLocked: 已锁定,右键解锁
+ ModdedSlotUnlocked: 右键以锁定状态
}
PlayerStats: {
diff --git a/Packets/Weather/WeatherLockSyncPacket.cs b/Packets/Weather/WeatherLockSyncPacket.cs
new file mode 100644
index 00000000..d3d500fe
--- /dev/null
+++ b/Packets/Weather/WeatherLockSyncPacket.cs
@@ -0,0 +1,31 @@
+using ImproveGame.Content.Functions.WeatherControl;
+
+namespace ImproveGame.Packets.Weather;
+
+///
+/// 通用气候控制锁定状态同步包
+///
+[AutoSync]
+public class WeatherLockSyncPacket : NetModule
+{
+ private string _id;
+ private bool _locked;
+
+ public static void Send(string id, bool locked)
+ {
+ if (string.IsNullOrEmpty(id)) return;
+ var module = NetModuleLoader.Get();
+ module._id = id;
+ module._locked = locked;
+ module.Send(runLocally: true);
+ }
+
+ public override void Receive()
+ {
+ if (WeatherControlRegistry.Instance.TryGet(_id, out var control))
+ control.SetLocked(_locked);
+
+ if (Main.netMode is NetmodeID.Server)
+ Send(-1, Sender);
+ }
+}
diff --git a/Packets/Weather/WeatherStageSyncPacket.cs b/Packets/Weather/WeatherStageSyncPacket.cs
new file mode 100644
index 00000000..fa3f41f9
--- /dev/null
+++ b/Packets/Weather/WeatherStageSyncPacket.cs
@@ -0,0 +1,32 @@
+using ImproveGame.Content.Functions.WeatherControl;
+
+namespace ImproveGame.Packets.Weather;
+
+///
+/// 通用气候控制档位同步包
+/// 任意注册项变更档位都走这一路,包内只带 id 与目标 stage
+///
+[AutoSync]
+public class WeatherStageSyncPacket : NetModule
+{
+ private string _id;
+ private int _stage;
+
+ public static void Send(string id, int stage)
+ {
+ if (string.IsNullOrEmpty(id)) return;
+ var module = NetModuleLoader.Get();
+ module._id = id;
+ module._stage = stage;
+ module.Send(runLocally: true);
+ }
+
+ public override void Receive()
+ {
+ if (WeatherControlRegistry.Instance.TryGet(_id, out var control))
+ control.SetStage(_stage);
+
+ if (Main.netMode is NetmodeID.Server)
+ Send(-1, Sender);
+ }
+}
diff --git a/README-en.md b/README-en.md
index c4effb51..f6aa582a 100644
--- a/README-en.md
+++ b/README-en.md
@@ -438,3 +438,164 @@ Add new house for Wand of Architecture
| Cyan | 00FFFF | NoWall |
| Green | 00FF00 | Door |
| Transparent | | Wall |
+
+### RegisterWeatherControl
+
+Register a control item into the Weather Control panel. Items without a group go into the default "modded" grid on the panel
+It is recommended to copy [ImproveGame_WeatherControlCrossModHelper.cs](ImproveGame_WeatherControlCrossModHelper.cs) into your mod and use the strongly-typed wrappers instead of raw `Mod.Call`
+
+#### Parameters
+
+- `Mod` The source mod instance, used as the ModName of the control
+- `string` Internal name of the control, combined with ModName as the unique id `ModName:Name`
+- `Texture2D` Slot icon, anything larger than 32x32 will be scaled down to fit
+- `Func/LocalizedText/string` Display name provider, shown as the first line of the hover tooltip
+- `Func/LocalizedText/string` Hover description, may be `null`
+- `string[]/IReadOnlyList` Stage name array with at least 2 entries. The index order is the stage order. Names are internal keys only and not displayed
+- `Func` Returns the current stage index, `-1` if unknown
+- `Action` Applies a stage locally, no networking required
+- `bool` Optional, whether the item supports locking, default `false`
+- `Func` Optional, returns the current locked state
+- `Action` Optional, applies the locked state locally
+- `Func` Optional, decides whether the item is currently available. Unavailable items are hidden from the panel
+- `int` Optional, sort priority, higher first, default `0`
+- `string` Optional, id of the group this item belongs to. Leave empty to fall into the default modded grid
+
+#### Return Value
+
+- `bool` Whether the registration succeeded. Returns `false` if stages are insufficient or required arguments are missing
+
+#### Notes
+
+- `setStage` and `setLocked` only need to write the local state. Network sync is handled by `SetWeatherControlStage` / `SetWeatherControlLocked`, which dispatch even in single player
+- Registering twice with the same `(ModName, Name)` overwrites the previous entry
+
+### UnregisterWeatherControl
+
+Remove a registered weather control item
+
+#### Parameters
+
+- `Mod` The source mod instance
+- `string` Internal name of the control
+
+### RegisterWeatherControlGroup
+
+Register a custom content group. A group lets your mod attach its own UI view to the Weather Control panel instead of dropping every item into the default modded grid
+
+#### Parameters
+
+- `Mod` The source mod instance
+- `string` Internal name of the group
+- `Func/LocalizedText/string` Display name provider
+- `int` Sort priority, higher first
+- `Func` Builds the group view, invoked once each time the Weather Control panel opens
+- `Func` Optional, decides whether the group is currently available
+
+### UnregisterWeatherControlGroup
+
+Remove a registered group
+
+#### Parameters
+
+- `Mod` The source mod instance
+- `string` Internal name of the group
+
+### QueryWeatherControlStage
+
+Read the current stage index of a control. Works on builtin items too
+
+#### Parameters
+
+- `string` Full id of the control, formatted as `ModName:Name`
+
+#### Return Value
+
+- `int` Current stage index, `-1` when the control does not exist
+
+### SetWeatherControlStage
+
+Dispatch a stage change. All clients apply the update
+
+#### Parameters
+
+- `string` Full id of the control
+- `int` Target stage index
+
+### IsWeatherControlLocked
+
+Read whether a control is currently locked
+
+#### Parameters
+
+- `string` Full id of the control
+
+#### Return Value
+
+- `bool` Whether the control is locked. Returns `false` when the control does not exist or does not support locking
+
+### SetWeatherControlLocked
+
+Dispatch a locked state change. Returns `false` if the target control does not support locking
+
+#### Parameters
+
+- `string` Full id of the control
+- `bool` Target locked state
+
+### Built-in Weather Control Ids
+
+| Id | Item | Stages |
+| --- | --- | --- |
+| `ImproveGame:Time` | Time of day | `Dawn` / `Noon` / `Dusk` / `Midnight` |
+| `ImproveGame:MoonPhase` | Moon phase | `Phase0` ~ `Phase7` |
+| `ImproveGame:Rain` | Rain | `Off` / `On` |
+| `ImproveGame:Sandstorm` | Sandstorm | `Off` / `On` |
+| `ImproveGame:Wind` | Wind direction | `West` / `No` / `East` |
+
+### Example
+
+The snippet below registers a custom weather state from another mod through the helper (copy [ImproveGame_WeatherControlCrossModHelper.cs](ImproveGame_WeatherControlCrossModHelper.cs) into your project first):
+
+```CSharp
+public override void PostSetupContent()
+{
+ if (!ModLoader.TryGetMod("ImproveGame", out var qot)) return;
+
+ ImproveGame_WeatherControlCrossModHelper.RegisterWeatherControl(
+ qot: qot,
+ source: this,
+ name: "ScorchingDay",
+ icon: TextureAssets.Item[ItemID.LivingFireBlock].Value,
+ displayName: () => Language.GetTextValue("Mods.MyMod.ScorchingDay.Name"),
+ tooltip: () => Language.GetTextValue("Mods.MyMod.ScorchingDay.Tooltip"),
+ stages: ["Inactive", "Active"],
+ getStage: () => MySystem.Active ? 1 : 0,
+ setStage: MySystem.ApplyStage,
+ supportsLock: true,
+ getLocked: () => MySystem.Locked,
+ setLocked: locked => MySystem.Locked = locked,
+ priority: 100);
+}
+
+public override void Unload()
+{
+ // Drop the registration so the Registry does not keep callbacks pointing into an unloaded assembly
+ if (ModLoader.TryGetMod("ImproveGame", out var qot))
+ ImproveGame_WeatherControlCrossModHelper.UnregisterWeatherControl(qot, this, "ScorchingDay");
+}
+```
+
+Stage transitions go through `SetWeatherControlStage`, which works on builtin items as well:
+
+```CSharp
+if (ModLoader.TryGetMod("ImproveGame", out var qot))
+{
+ // Start raining across all clients
+ ImproveGame_WeatherControlCrossModHelper.SetWeatherControlStage(
+ qot, ImproveGame_WeatherControlCrossModHelper.RainId, 1);
+
+ int now = ImproveGame_WeatherControlCrossModHelper.QueryWeatherControlStage(
+ qot, ImproveGame_WeatherControlCrossModHelper.RainId);
+}
+```
diff --git a/UI/WeatherControl/Components/ModdedWeatherItemsView.cs b/UI/WeatherControl/Components/ModdedWeatherItemsView.cs
new file mode 100644
index 00000000..83aa0eec
--- /dev/null
+++ b/UI/WeatherControl/Components/ModdedWeatherItemsView.cs
@@ -0,0 +1,56 @@
+using ImproveGame.Content.Functions.WeatherControl;
+using ImproveGame.UIFramework.BaseViews;
+
+namespace ImproveGame.UI.WeatherControl.Components;
+
+///
+/// 模组项默认网格栏
+/// 浮在艺术画面左上角的天空区域,让外部模组的控制项跟原版控件一样长在场景里
+///
+public class ModdedWeatherItemsView : View
+{
+ private int _builtCount = -1;
+
+ public ModdedWeatherItemsView()
+ {
+ SetPadding(0f);
+ IsAdaptiveWidth = true;
+ Height.Set(32f, 0f);
+ Left.Pixels = 8f;
+ Top.Pixels = 8f;
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ var registry = WeatherControlRegistry.Instance;
+ if (registry is null) return;
+
+ int count = 0;
+ foreach (var _ in registry.ModdedGridControls()) count++;
+
+ if (count != _builtCount)
+ Rebuild(registry);
+ }
+
+ private void Rebuild(WeatherControlRegistry registry)
+ {
+ RemoveAllChildren();
+ _builtCount = 0;
+
+ foreach (var control in registry.ModdedGridControls())
+ {
+ var slot = new WeatherControlSlot(control)
+ {
+ RelativeMode = RelativeMode.Horizontal,
+ Spacing = new Vector2(4f, 0f),
+ };
+ slot.JoinParent(this);
+ _builtCount++;
+ }
+
+ // 通知父级重算,让自适应宽度的 MainPanel 和槽位坐标都更新
+ Parent?.Recalculate();
+ }
+}
diff --git a/UI/WeatherControl/Components/WeatherControlSlot.cs b/UI/WeatherControl/Components/WeatherControlSlot.cs
new file mode 100644
index 00000000..aa78c7fb
--- /dev/null
+++ b/UI/WeatherControl/Components/WeatherControlSlot.cs
@@ -0,0 +1,99 @@
+using ImproveGame.Content.Functions.WeatherControl;
+using ImproveGame.UIFramework.BaseViews;
+using ImproveGame.UIFramework.Common;
+using ImproveGame.UIFramework.SUIElements;
+using Terraria.ModLoader.UI;
+
+namespace ImproveGame.UI.WeatherControl.Components;
+
+///
+/// 模组项栏里的一个槽位,绑定一个
+/// 左键循环档位,右键切换锁定
+///
+public class WeatherControlSlot : TimerView
+{
+ public readonly IWeatherControl Control;
+
+ private readonly SUIImage _icon;
+
+ public WeatherControlSlot(IWeatherControl control)
+ {
+ Control = control;
+
+ // 嵌在艺术画面里,所以用更小的尺寸和半透明边框,避免抢镜
+ Border = 1f;
+ Rounded = new Vector4(4f);
+ SetSizePixels(32f, 32f);
+
+ _icon = new SUIImage(control?.Icon, false)
+ {
+ ImageAlign = new Vector2(0.5f),
+ ImageScale = 1f,
+ };
+ _icon.SetSizePercent(1f, 1f);
+ _icon.JoinParent(this);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ if (Control is null) return;
+
+ var tex = Control.Icon;
+ _icon.Texture = tex;
+ // 给槽位留两像素的呼吸空间,超过的图标按比例缩放
+ if (tex != null)
+ {
+ float maxSide = Math.Max(tex.Width, tex.Height);
+ _icon.ImageScale = maxSide > 28f ? 28f / maxSide : 1f;
+ }
+
+ BgColor = Color.Black * HoverTimer.Lerp(0.25f, 0.55f);
+ BorderColor = Control.GetLocked()
+ ? UIStyle.ItemSlotBorderFav
+ : Color.White * HoverTimer.Lerp(0.15f, 0.45f);
+ }
+
+ public override void LeftMouseDown(UIMouseEvent evt)
+ {
+ base.LeftMouseDown(evt);
+ if (Control is null) return;
+
+ int count = Control.Stages.Count;
+ if (count < 2) return;
+
+ int current = Control.GetStage();
+ if (current < 0 || current >= count) current = -1;
+ int next = (current + 1) % count;
+ SoundEngine.PlaySound(SoundID.MenuTick);
+ WeatherControlRegistry.Instance.DispatchStage(Control.Id, next);
+ }
+
+ public override void RightMouseDown(UIMouseEvent evt)
+ {
+ base.RightMouseDown(evt);
+ if (Control is null || !Control.SupportsLock) return;
+
+ bool target = !Control.GetLocked();
+ SoundEngine.PlaySound(SoundID.MenuTick);
+ WeatherControlRegistry.Instance.DispatchLocked(Control.Id, target);
+ }
+
+ public override void DrawSelf(SpriteBatch spriteBatch)
+ {
+ base.DrawSelf(spriteBatch);
+
+ if (Control is null || !IsMouseHovering) return;
+
+ var sb = new System.Text.StringBuilder();
+ sb.Append(Control.GetDisplayName() ?? Control.Name);
+ var tip = Control.GetTooltip();
+ if (!string.IsNullOrEmpty(tip)) sb.Append('\n').Append(tip);
+ if (Control.SupportsLock)
+ sb.Append('\n').Append(Control.GetLocked()
+ ? GetText("UI.WeatherGUI.ModdedSlotLocked")
+ : GetText("UI.WeatherGUI.ModdedSlotUnlocked"));
+ UICommon.TooltipMouseText(sb.ToString());
+ }
+}
diff --git a/UI/WeatherControl/WeatherAmbientElement.cs b/UI/WeatherControl/WeatherAmbientElement.cs
index fb1f9498..c9c660b5 100644
--- a/UI/WeatherControl/WeatherAmbientElement.cs
+++ b/UI/WeatherControl/WeatherAmbientElement.cs
@@ -472,7 +472,7 @@ private static void DrawPinWheel(Rectangle boundary, Color color)
{
Main.spriteBatch.Draw(pinHighlight, pinPosition, color);
_hoverText = GetText("UI.WeatherGUI.Wind") + "\n" +
- GetText($"UI.WeatherGUI.WindLock{WeatherController.MoonPhaseLocked}");
+ GetText($"UI.WeatherGUI.WindLock{WeatherController.WindLocked}");
}
Main.spriteBatch.Draw(wheelTexture, center, null, color, _pinWheelRotation,
diff --git a/UI/WeatherControl/WeatherGUI.cs b/UI/WeatherControl/WeatherGUI.cs
index e3cb949e..9ec628fa 100644
--- a/UI/WeatherControl/WeatherGUI.cs
+++ b/UI/WeatherControl/WeatherGUI.cs
@@ -1,4 +1,6 @@
-using ImproveGame.UIFramework;
+using ImproveGame.Content.Functions.WeatherControl;
+using ImproveGame.UI.WeatherControl.Components;
+using ImproveGame.UIFramework;
using ImproveGame.UIFramework.BaseViews;
using ImproveGame.UIFramework.Common;
using ImproveGame.UIFramework.SUIElements;
@@ -29,6 +31,12 @@ public override bool CanSetFocusTarget(UIElement target)
// 天气环境
private WeatherAmbientElement WeatherAmbient;
+ // 外部模组项默认网格栏
+ private ModdedWeatherItemsView ModdedItemsView;
+
+ // 注册的外部内容栏
+ private readonly List _customGroupViews = [];
+
public override void OnInitialize()
{
// 主面板
@@ -100,9 +108,39 @@ public override void OnInitialize()
};
WeatherAmbient.JoinParent(bottomArea);
+ // 把模组项浮窗直接挂到艺术画面里,让外部图标融入场景
+ ModdedItemsView = new ModdedWeatherItemsView();
+ ModdedItemsView.JoinParent(bottomArea);
+
+ BuildCustomGroupViews();
+
Recalculate();
}
+ private void BuildCustomGroupViews()
+ {
+ foreach (var view in _customGroupViews)
+ MainPanel.RemoveChild(view);
+ _customGroupViews.Clear();
+
+ var registry = WeatherControlRegistry.Instance;
+ if (registry is null) return;
+
+ foreach (var group in registry.SortedGroups())
+ {
+ if (!group.IsAvailable) continue;
+ var controls = registry.ControlsInGroup(group.Id).ToList();
+ var view = group.CreateView(controls);
+ if (view is null) continue;
+
+ if (view is View v) v.RelativeMode = RelativeMode.Vertical;
+ view.Recalculate();
+
+ MainPanel.Append(view);
+ _customGroupViews.Add(view);
+ }
+ }
+
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
@@ -118,6 +156,8 @@ public void Open()
{
SoundEngine.PlaySound(SoundID.MenuOpen);
Visible = true;
+ BuildCustomGroupViews();
+ Recalculate();
StartTimer.Open();
}