Skip to content
Merged
8 changes: 8 additions & 0 deletions RealTimeWeatherMod/ChillEnvPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ public class ChillEnvPlugin : BaseUnityPlugin

// --- 配置项 ---
internal static ConfigEntry<int> Cfg_WeatherRefreshMinutes;
internal static ConfigEntry<int> Cfg_CacheExpiryMinutes;
internal static ConfigEntry<string> Cfg_SunriseTime;
internal static ConfigEntry<string> Cfg_SunsetTime;
internal static ConfigEntry<string> Cfg_WeatherProvider;
internal static ConfigEntry<string> Cfg_SeniverseKey;
internal static ConfigEntry<string> Cfg_Location;
internal static ConfigEntry<double> Cfg_OpenMeteoLatitude;
internal static ConfigEntry<double> Cfg_OpenMeteoLongitude;
internal static ConfigEntry<bool> Cfg_EnableTimeSync;
internal static ConfigEntry<bool> Cfg_EnableWeatherSync;
internal static ConfigEntry<bool> Cfg_UnlockEnvironments;
Expand Down Expand Up @@ -97,13 +101,17 @@ private void Awake()
private void InitConfig()
{
Cfg_WeatherRefreshMinutes = Config.Bind("WeatherSync", "RefreshMinutes", 30, "天气API刷新间隔(分钟)");
Cfg_CacheExpiryMinutes = Config.Bind("WeatherSync", "CacheExpiryMinutes", 60, "天气缓存有效期(分钟)");
Cfg_SunriseTime = Config.Bind("TimeConfig", "Sunrise", "06:30", "日出时间");
Cfg_SunsetTime = Config.Bind("TimeConfig", "Sunset", "18:30", "日落时间");
Cfg_EnableTimeSync = Config.Bind("TimeSync", "EnableTimeSync", true, "是否启用时间同步");

Cfg_EnableWeatherSync = Config.Bind("WeatherAPI", "EnableWeatherSync", false, "是否启用天气API同步");
Cfg_WeatherProvider = Config.Bind("WeatherAPI", "WeatherProvider", "OpenMeteo", "天气数据源: Seniverse 或 OpenMeteo");
Cfg_SeniverseKey = Config.Bind("WeatherAPI", "SeniverseKey", "", "心知天气 API Key");
Cfg_Location = Config.Bind("WeatherAPI", "Location", "beijing", "城市名称");
Cfg_OpenMeteoLatitude = Config.Bind("WeatherAPI", "Latitude", 39.9042, "Open-Meteo 纬度");
Cfg_OpenMeteoLongitude = Config.Bind("WeatherAPI", "Longitude", 116.4074, "Open-Meteo 经度");

Cfg_UnlockEnvironments = Config.Bind("Unlock", "UnlockAllEnvironments", true, "自动解锁环境");
Cfg_UnlockDecorations = Config.Bind("Unlock", "UnlockAllDecorations", true, "自动解锁装饰道具");
Expand Down
12 changes: 12 additions & 0 deletions RealTimeWeatherMod/Core/AutoEnvRunner.StartupSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ private System.Collections.IEnumerator EarlyStartupSync()
_pendingStartupWeather = WeatherService.CachedWeather;
_startupWeatherFetchFinished = true;
UpdateUiWeatherString(_pendingStartupWeather);
CheckAndSyncSunSchedule();
}
else if (needWeatherFetch)
{
Expand All @@ -475,12 +476,23 @@ private System.Collections.IEnumerator EarlyStartupSync()
{
_pendingStartupWeather = weather;
UpdateUiWeatherString(weather);
(weather) =>
{
_startupWeatherFetchFinished = true;
if (weather != null)
{
_pendingStartupWeather = weather;
UpdateUiWeatherString(weather);
}
CheckAndSyncSunSchedule();
}
}
}));
}
else
{
_startupWeatherFetchFinished = true;
CheckAndSyncSunSchedule();
}

float timeout = 30f;
Expand Down
69 changes: 35 additions & 34 deletions RealTimeWeatherMod/Core/AutoEnvRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public partial class AutoEnvRunner : MonoBehaviour
private EnvironmentType? _lastAppliedEnv;
private bool _isFetching;
private bool _pendingForceRefresh;
private float _nextSunSyncAttemptTime;

private static AutoEnvRunner _instance;

Expand All @@ -29,7 +30,6 @@ private void Start()
_nextTimeCheckTime = Time.time + 10f;
ChillEnvPlugin.Log?.LogInfo("Runner 启动...");

CheckAndSyncSunSchedule();
StartCoroutine(EarlyStartupSync());
}

Expand All @@ -44,7 +44,7 @@ private SyncPolicySnapshot BuildPolicySnapshot()
private bool HasUsableApiKey()
{
string apiKey = ChillEnvPlugin.Cfg_SeniverseKey.Value;
return !string.IsNullOrEmpty(apiKey) || WeatherService.HasDefaultKey;
return WeatherService.HasUsableProvider(ChillEnvPlugin.Cfg_WeatherProvider.Value, apiKey);
}

private void UpdateUiWeatherString(WeatherInfo weather)
Expand Down Expand Up @@ -97,52 +97,40 @@ private void CheckAndSyncSunSchedule()
string lastSync = ChillEnvPlugin.Cfg_LastSunSyncDate.Value;
string today = DateTime.Now.ToString("yyyy-MM-dd");

if (lastSync != today)
if (lastSync == today || Time.time < _nextSunSyncAttemptTime)
{
StartCoroutine(SyncSunScheduleRoutine(today));
return;
}

_nextSunSyncAttemptTime = Time.time + GetConfiguredWeatherRefreshSeconds();
StartCoroutine(SyncSunScheduleRoutine(today));
Comment thread
Tim-Devil marked this conversation as resolved.
Comment thread
Tim-Devil marked this conversation as resolved.
}

private System.Collections.IEnumerator SyncSunScheduleRoutine(string targetDate)
{
int retryCount = 0;
float delay = 1f;
const int MaxRetries = 10;
bool success = false;
string apiKey = ChillEnvPlugin.Cfg_SeniverseKey.Value;
string location = ChillEnvPlugin.Cfg_Location.Value;

while (retryCount < MaxRetries)
yield return WeatherService.FetchSunSchedule(apiKey, location, (data) =>
{
bool success = false;
string apiKey = ChillEnvPlugin.Cfg_SeniverseKey.Value;
string location = ChillEnvPlugin.Cfg_Location.Value;

yield return WeatherService.FetchSunSchedule(apiKey, location, (data) =>
if (data != null)
{
if (data != null)
{
ChillEnvPlugin.Log?.LogInfo($"[SunSync] 同步成功: 日出{data.sunrise} 日落{data.sunset}");
ChillEnvPlugin.Log?.LogInfo($"[SunSync] 同步成功: 日出{data.sunrise} 日落{data.sunset}");

ChillEnvPlugin.Cfg_SunriseTime.Value = data.sunrise;
ChillEnvPlugin.Cfg_SunsetTime.Value = data.sunset;
ChillEnvPlugin.Cfg_LastSunSyncDate.Value = targetDate;

ChillEnvPlugin.Instance.Config.Save();
success = true;
}
});
ChillEnvPlugin.Cfg_SunriseTime.Value = data.sunrise;
ChillEnvPlugin.Cfg_SunsetTime.Value = data.sunset;
ChillEnvPlugin.Cfg_LastSunSyncDate.Value = targetDate;

if (success)
{
yield break;
ChillEnvPlugin.Instance.Config.Save();
success = true;
}
});

ChillEnvPlugin.Log?.LogWarning($"[SunSync] 同步失败,{delay}秒后重试 ({retryCount + 1}/{MaxRetries})");
yield return new WaitForSeconds(delay);

delay *= 2f;
retryCount++;
if (!success)
{
ChillEnvPlugin.Log?.LogWarning("[SunSync] 同步失败,保留现有日出日落设置");
}

ChillEnvPlugin.Log?.LogError("[SunSync] 达到最大重试次数,今日放弃同步");
}

private void Update()
Expand Down Expand Up @@ -299,6 +287,7 @@ private void TriggerSync(bool forceApi, bool forceApply)
if (!forceApi && hasValidWeatherCache)
{
UpdateUiWeatherString(WeatherService.CachedWeather);
CheckAndSyncSunSchedule();
ScheduleNextWeatherCheckFromCache(location);
return;
}
Expand All @@ -325,6 +314,11 @@ private void TriggerSync(bool forceApi, bool forceApply)
UpdateUiWeatherString(weather);
if (weather != null)
{
if (WeatherService.LastFetchSucceeded)
{
CheckAndSyncSunSchedule();
}

ScheduleNextWeatherCheckFromCache(location);
}
else
Expand All @@ -338,6 +332,7 @@ private void TriggerSync(bool forceApi, bool forceApply)
else if (policy.NeedWeatherDataForUI && hasValidWeatherCache)
{
UpdateUiWeatherString(WeatherService.CachedWeather);
CheckAndSyncSunSchedule();
ScheduleNextWeatherCheckFromCache(location);
}
else
Expand Down Expand Up @@ -377,6 +372,11 @@ private void TriggerSync(bool forceApi, bool forceApply)
ApplyByPolicy(policy, weather, forceApply);
if (weather != null)
{
if (WeatherService.LastFetchSucceeded)
{
CheckAndSyncSunSchedule();
}

ScheduleNextWeatherCheckFromCache(location);
}
else
Expand Down Expand Up @@ -404,6 +404,7 @@ private void TriggerSync(bool forceApi, bool forceApply)

if (shouldFetchWeather && hasValidWeatherCache)
{
CheckAndSyncSunSchedule();
ScheduleNextWeatherCheckFromCache(location);
}
else
Expand Down
1 change: 1 addition & 0 deletions RealTimeWeatherMod/RealTimeWeather.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\DecorationUnlockScanner.cs" />
<Compile Include="Services\KeySecurity.cs" />
<Compile Include="Services\OpenMeteoWeatherMapper.cs" />
<Compile Include="Services\WeatherService.cs" />
<Compile Include="Utils\EnvRegistry.cs" />
<Compile Include="Utils\WindowViewStateAccessor.cs" />
Expand Down
125 changes: 125 additions & 0 deletions RealTimeWeatherMod/Services/OpenMeteoWeatherMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using ChillWithYou.EnvSync.Models;

namespace ChillWithYou.EnvSync.Services
{
internal static class OpenMeteoWeatherMapper
{
internal static WeatherInfo Map(
int weatherCode,
double temperature,
double precipitation,
double rain,
double showers,
double snowfall,
double cloudCover,
DateTime updateTime)
{
int normalizedCode = ToSeniverseCode(weatherCode, precipitation, rain, showers, snowfall, cloudCover);
return new WeatherInfo
{
Code = normalizedCode,
Text = ToWeatherText(weatherCode, normalizedCode),
Temperature = (int)Math.Round(temperature),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.Round without a MidpointRounding argument defaults to banker's rounding (round-to-even). For example, 2.5°C rounds to 2 and 3.5°C rounds to 4, which can be counterintuitive for weather display. Consider using MidpointRounding.AwayFromZero for temperatures: Math.Round(temperature, MidpointRounding.AwayFromZero).

Suggestion:

Suggested change
Temperature = (int)Math.Round(temperature),
Temperature = (int)Math.Round(temperature, MidpointRounding.AwayFromZero),

Condition = ToCondition(normalizedCode),
UpdateTime = updateTime
};
}

internal static int ToSeniverseCode(
int weatherCode,
double precipitation,
double rain,
double showers,
double snowfall,
double cloudCover)
{
if (snowfall > 0d || IsSnow(weatherCode))
{
return 21;
}

if (IsThunder(weatherCode))
{
return 11;
}

if (IsHeavyRain(weatherCode) || precipitation >= 2.5d || rain + showers >= 2.5d)
{
return 14;
}

if (IsLightRain(weatherCode) || precipitation > 0d || rain > 0d || showers > 0d)
{
return 13;
}

if (weatherCode == 45 || weatherCode == 48)
{
return 26;
}

// Codes 1-3 (partly cloudy/overcast) or clear sky with high measured cloud cover → Cloudy
if ((weatherCode >= 1 && weatherCode <= 3) || cloudCover >= 65d)
{
return 4;
}

return 1;
Comment thread
Tim-Devil marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no conditions match, ToSeniverseCode silently returns 1 (Clear). Unrecognized weather codes are effectively hidden, making it harder to detect API changes or mapping gaps during development. Consider logging a warning via ChillEnvPlugin.Log?.LogWarning(...) when an unmapped code is encountered, or alternatively have the caller in WeatherService.ParseOpenMeteoWeatherJson validate the result.

Suggestion:

Suggested change
return 1;
// Fallback: treat as clear. Log a warning if the weatherCode was not explicitly 0.
if (weatherCode != 0)
{
ChillEnvPlugin.Log?.LogWarning($"[OpenMeteo] Unmapped weather code {weatherCode}, falling back to Clear");
}
return 1;

}

private static bool IsLightRain(int weatherCode)
{
return weatherCode == 51 ||
weatherCode == 53 ||
weatherCode == 55 ||
weatherCode == 56 ||
weatherCode == 57 ||
weatherCode == 61 ||
weatherCode == 63 ||
weatherCode == 80;
}

private static bool IsHeavyRain(int weatherCode)
{
return weatherCode == 65 ||
weatherCode == 66 ||
weatherCode == 67 ||
weatherCode == 81 ||
weatherCode == 82;
}

private static bool IsSnow(int weatherCode)
{
return weatherCode == 71 ||
weatherCode == 73 ||
weatherCode == 75 ||
weatherCode == 77 ||
weatherCode == 85 ||
weatherCode == 86;
}

private static bool IsThunder(int weatherCode)
{
return weatherCode == 95 ||
weatherCode == 96 ||
weatherCode == 99;
}

private static WeatherCondition ToCondition(int normalizedCode)
{
return WeatherService.MapCodeToCondition(normalizedCode);
}
Comment thread
Tim-Devil marked this conversation as resolved.

private static string ToWeatherText(int weatherCode, int normalizedCode)
{
if (normalizedCode == 21) return "Snow";
if (normalizedCode == 11) return "ThunderRain";
if (normalizedCode == 14) return "HeavyRain";
if (normalizedCode == 13) return "LightRain";
if (normalizedCode == 26) return "Fog";
if (normalizedCode == 4) return "Cloudy";
return weatherCode == 0 ? "Clear" : "Unknown";
Comment on lines +114 to +122

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToWeatherText uses the raw weatherCode in its fallback, which can produce "Unknown" when normalizedCode=1 but weatherCode≠0. Meanwhile, ToCondition(1) returns WeatherCondition.Clear via MapCodeToCondition, creating an inconsistency. Add an explicit case for normalizedCode=1 so the text is fully determined by the normalized code, and consider logging a warning for unrecognized weatherCode values.

Suggestion:

Suggested change
private static string ToWeatherText(int weatherCode, int normalizedCode)
{
if (normalizedCode == 21) return "Snow";
if (normalizedCode == 11) return "ThunderRain";
if (normalizedCode == 14) return "HeavyRain";
if (normalizedCode == 13) return "LightRain";
if (normalizedCode == 26) return "Fog";
if (normalizedCode == 4) return "Cloudy";
return weatherCode == 0 ? "Clear" : "Unknown";
private static string ToWeatherText(int weatherCode, int normalizedCode)
{
if (normalizedCode == 21) return "Snow";
if (normalizedCode == 11) return "ThunderRain";
if (normalizedCode == 14) return "HeavyRain";
if (normalizedCode == 13) return "LightRain";
if (normalizedCode == 26) return "Fog";
if (normalizedCode == 4) return "Cloudy";
if (normalizedCode == 1) return "Clear";
return "Unknown";

}
}
}
Loading
Loading