Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@
开启彩蛋功能后,系统可能会随机或者在满足特定条件后主动开启某些风景效果。

### 声音效果
开启独立的环境音效托管开关 `EnableAmbientSounds`(默认关闭)后,系统会按条件管理声音效果。
开启 `Automation.EnableAmbientSounds`(默认关闭)后,系统会按条件托管做饭、空调和蝉鸣;关闭时沿用原有托管清理与用户接管规则,不是全局静音。

雨声由独立的 `Automation.EnablePrecipitationSounds` 控制(默认开启,旧配置缺少此项时保留原有雨声行为)。设为 `false` 后,天气同步仍切换小雨、大雨、雷雨画面,但会静音对应雨声,不影响角色语音、动作声、其他环境音或雪景。天气同步开启时,在设置面板切换会立即更新当前雨声,无需刷新天气;过场或鲸鱼覆盖期间仍遵循同步保护。修改配置文件后按 F7 重载并同步。两项开关可以独立组合,手动雨景操作仍由游戏控制。

### TODO

Expand Down Expand Up @@ -134,8 +136,10 @@
[Automation]
## 启用季节性景色彩蛋自动托管
EnableSeasonalEasterEggs = true
## 启用环境音效自动托管
## 启用做饭、空调、蝉鸣环境音效自动托管(不控制雨声)
EnableAmbientSounds = false
## 天气同步时开启降水声音;false 保留雨景并静音雨声,设置面板切换立即更新当前雨声
EnablePrecipitationSounds = true
## 跟随游戏内实际雨景,阻止角色自动开窗;已开窗时,小雨 30–90 秒、雨 3–8 秒、雷雨 1–4 秒后请求关窗,等待现有动作完成
CloseWindowWhenRaining = true

Expand Down
8 changes: 6 additions & 2 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ When seasonal easter eggs are enabled, some scenery effects may be activated ran

### Sound Effects

When the separate EnableAmbientSounds switch is enabled (off by default), ambient sound effects are managed according to their conditions.
`Automation.EnableAmbientSounds` (off by default) automates cooking, air conditioner and cicada sounds according to their conditions. Disabling it retains the existing ownership and cleanup rules; it is not a global mute.

`Automation.EnablePrecipitationSounds` separately controls rain audio. It defaults to `true`, preserving rain audio for existing configs without this key. Set it to `false` to keep synchronized light, heavy and thunderstorm rain visuals while muting their sounds, without affecting character voices, action sounds, other ambient audio or snow. With weather sync enabled, the settings-panel toggle updates current rain audio immediately without refreshing weather; cutscene and whale overrides still take priority. After editing the config file, press F7 to reload and sync. Both switches are independent. Manual rain controls retain the game's behavior.

### TODO

Expand Down Expand Up @@ -147,8 +149,10 @@ After first run, config is generated at `BepInEx/config/chillwithyou.envsync.cfg
[Automation]
## Enable automatic seasonal scenery easter eggs
EnableSeasonalEasterEggs = true
## Enable automatic ambient sound management
## Automate cooking, air conditioner and cicada sounds (does not control rain audio)
EnableAmbientSounds = false
## Play precipitation sounds during weather sync; false keeps rain visuals and mutes rain audio; panel changes update current rain audio immediately
EnablePrecipitationSounds = true
## Follow active in-game rain; block opening and request closing after 30–90s in light rain, 3–8s in rain, or 1–4s in thunderstorms, allowing existing actions to finish
CloseWindowWhenRaining = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@ namespace RealTimeWeatherMod.Tests.Core
{
public class CombinedEnvironmentStatePolicyTests
{
[Theory]
[InlineData(false, false, false, true, false)]
[InlineData(false, true, true, true, true)]
[InlineData(true, true, true, false, true)]
[InlineData(true, false, true, false, true)]
[InlineData(true, true, false, false, true)]
[InlineData(true, false, false, false, false)]
public void SilentRain_OpensOrKeepsWindow_AndMutesStoredOrLiveSound(
bool windowActive, bool soundActive, bool liveSoundActive,
bool changeWindow, bool changeSound)
{
Assert.Equal(changeWindow, CombinedEnvironmentStatePolicy.NeedsWindowChange(windowActive, true));
Assert.Equal(changeSound, CombinedEnvironmentStatePolicy.NeedsSoundChange(
true, soundActive, liveSoundActive, false));
Assert.Equal(!changeWindow && !changeSound, CombinedEnvironmentStatePolicy.IsInTargetState(
windowActive, true, soundActive, liveSoundActive, true, targetSoundState: false));
}

[Fact]
public void SilentRain_Stopping_ClosesWindowWithoutMutingAgain()
{
Assert.True(CombinedEnvironmentStatePolicy.NeedsWindowChange(true, false));
Assert.False(CombinedEnvironmentStatePolicy.NeedsSoundChange(true, false, false, false));
Assert.False(CombinedEnvironmentStatePolicy.IsInTargetState(
true, true, false, false, false, targetSoundState: false));
Assert.True(CombinedEnvironmentStatePolicy.IsInTargetState(
false, true, false, false, false, targetSoundState: false));
}

[Fact]
public void EnablingRainSounds_WhenWindowAlreadyOpen_ChangesOnlySound()
{
Assert.False(CombinedEnvironmentStatePolicy.NeedsWindowChange(true, true));
Assert.True(CombinedEnvironmentStatePolicy.NeedsSoundChange(true, false, false, true));
Assert.False(CombinedEnvironmentStatePolicy.IsInTargetState(
true, true, false, false, true, targetSoundState: true));
Assert.True(CombinedEnvironmentStatePolicy.IsInTargetState(
true, true, true, true, true, targetSoundState: true));
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void ViewOnlyScenery_DoesNotRequireSound_WithEitherSoundTarget(bool soundTarget)
{
Assert.True(CombinedEnvironmentStatePolicy.IsInTargetState(
true, false, false, false, true, targetSoundState: soundTarget));
Assert.False(CombinedEnvironmentStatePolicy.NeedsSoundChange(false, false, false, soundTarget));
}

[Fact]
public void Closing_WhenOnlySoundRemains_ChangesOnlySound()
{
Expand Down
4 changes: 3 additions & 1 deletion RealTimeWeatherMod/ChillEnvPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class ChillEnvPlugin : BaseUnityPlugin

internal static ConfigEntry<bool> Cfg_EnableEasterEggs;
internal static ConfigEntry<bool> Cfg_EnableAmbientSounds;
internal static ConfigEntry<bool> Cfg_EnablePrecipitationSounds;
internal static ConfigEntry<bool> Cfg_CloseWindowWhenRaining;

// 调试配置
Expand Down Expand Up @@ -130,7 +131,8 @@ private void InitConfig()
Cfg_DetailedTimeSegments = Config.Bind("UI", "DetailedTimeSegments", true, "开启12小时制时,显示详细时段(凌晨/清晨/上午等)");

Cfg_EnableEasterEggs = Config.Bind("Automation", "EnableSeasonalEasterEggs", true, "启用季节性景色彩蛋自动托管");
Cfg_EnableAmbientSounds = Config.Bind("Automation", "EnableAmbientSounds", false, "启用环境音效自动托管");
Cfg_EnableAmbientSounds = Config.Bind("Automation", "EnableAmbientSounds", false, "启用做饭、空调、蝉鸣环境音效自动托管(不控制雨声)");
Cfg_EnablePrecipitationSounds = Config.Bind("Automation", "EnablePrecipitationSounds", true, "天气同步时开启小雨、大雨、雷雨声音;关闭时只保留雨景,不影响其他音效;天气同步开启时,设置面板切换立即更新当前雨声");
Cfg_CloseWindowWhenRaining = Config.Bind(
"Automation",
"CloseWindowWhenRaining",
Expand Down
84 changes: 69 additions & 15 deletions RealTimeWeatherMod/Core/GameEnvironmentRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,27 +153,35 @@ private bool InvokeBase(EnvironmentType target, EnvironmentApplyMode mode)
internal bool TryApplySceneryState(
EnvironmentType environmentType,
bool targetState,
EnvironmentApplyMode mode)
EnvironmentApplyMode mode,
bool applyPrecipitationSoundSetting = true)
{
bool mutePrecipitation = applyPrecipitationSoundSetting && IsPrecipitation(environmentType) &&
!(ChillEnvPlugin.Cfg_EnablePrecipitationSounds?.Value ?? true);
bool targetSoundState = targetState && !mutePrecipitation;
if (TryGetController(environmentType, out var controller))
{
if (TryIsControllerInTargetState(controller, targetState, out var isInTargetState))
if (TryIsControllerInTargetState(controller, targetState, targetSoundState, out var isInTargetState))
{
if (isInTargetState || TrySetControllerCombinedState(controller, targetState))
if (isInTargetState || TrySetControllerCombinedState(controller, targetState, targetSoundState))
{
return true;
}
}

if (mode == EnvironmentApplyMode.Interactive)
if (mode == EnvironmentApplyMode.Interactive && !mutePrecipitation)
{
return TrySimulateMainIconClick(environmentType);
}
}

if (mode == EnvironmentApplyMode.EarlyStartup)
// The main icon toggles BOTH view and sound, so it cannot represent silent rain.
// Use the direct window path and reconcile live sound if a controller exists.
if (mode == EnvironmentApplyMode.EarlyStartup || mutePrecipitation)
{
return TryApplySceneryStateDirect(environmentType, targetState);
bool windowApplied = TryApplySceneryStateDirect(environmentType, targetState, targetSoundState);
bool soundApplied = !mutePrecipitation || TrySetAmbientSoundState(environmentType, false, out _);
return windowApplied && soundApplied;
}

if (TryIsWindowActive(environmentType, out var isActive) && isActive == targetState)
Expand All @@ -184,6 +192,44 @@ internal bool TryApplySceneryState(
return TrySimulateMainIconClick(environmentType);
}

internal void RefreshPrecipitationSounds()
{
if (!ChillEnvPlugin.Cfg_EnableWeatherSync.Value ||
ChillEnvPlugin.IsInCutscene() || SceneryAutomationSystem.IsWhaleSystemTriggered)
{
return;
}

bool soundsEnabled = ChillEnvPlugin.Cfg_EnablePrecipitationSounds.Value;
foreach (var environmentType in ModEnvironmentMap.SceneryEnvironments)
{
if (!IsPrecipitation(environmentType)) continue;

// Preserve the current view, including rain that is still fading out.
if (TryGetController(environmentType, out var controller) &&
TryGetControllerCombinedState(controller, out var windowActive, out _, out _, out _) &&
TrySetControllerCombinedState(controller, windowActive, windowActive && soundsEnabled))
{
continue;
}

if (TryIsWindowActive(environmentType, out var fallbackWindowActive) &&
TrySetAmbientSoundState(environmentType, fallbackWindowActive && soundsEnabled, out _))
{
continue;
}

ChillEnvPlugin.Log?.LogWarning($"[环境运行时] 无法立即更新降水声音: {environmentType}");
}
}

private static bool IsPrecipitation(EnvironmentType environmentType)
{
return environmentType == EnvironmentType.LightRain ||
environmentType == EnvironmentType.HeavyRain ||
environmentType == EnvironmentType.ThunderRain;
}

internal ModEnvironment? GetActiveRain()
{
if (TryIsWindowActive(EnvironmentType.ThunderRain, out var thunder) && thunder)
Expand Down Expand Up @@ -535,7 +581,8 @@ internal bool TryGetRuntimeServices(
}
}

private bool TryApplySceneryStateDirect(EnvironmentType environmentType, bool targetState)
private bool TryApplySceneryStateDirect(
EnvironmentType environmentType, bool targetState, bool targetSoundState)
{
if (!TryGetRuntimeServices(out _, out var windowViewService, out var environmentDataService) ||
!EnsureDirectSceneryMetadata(windowViewService, environmentDataService) ||
Expand Down Expand Up @@ -570,12 +617,12 @@ private bool TryApplySceneryStateDirect(EnvironmentType environmentType, bool ta
environmentType.ToString(),
out var ambientEnumValue) &&
TryGetDirectSoundActive(environmentDataService, ambientEnumValue, out var isSoundActive) &&
isSoundActive != targetState)
isSoundActive != targetSoundState)
{
TryInvoke(
return TryInvoke(
_setMuteMethod,
environmentDataService,
new object[] { ambientEnumValue, !targetState },
new object[] { ambientEnumValue, !targetSoundState },
"[环境运行时] 启动期环境音状态写入失败");
}

Expand Down Expand Up @@ -652,6 +699,7 @@ private bool TryGetDirectSoundActive(
private bool TryIsControllerInTargetState(
EnvironmentController controller,
bool targetState,
bool targetSoundState,
out bool isInTargetState)
{
isInTargetState = false;
Expand All @@ -670,11 +718,13 @@ private bool TryIsControllerInTargetState(
hasSound,
soundActive,
liveSoundActive,
targetState);
targetState,
targetSoundState);
return true;
}

private bool TrySetControllerCombinedState(EnvironmentController controller, bool targetState)
private bool TrySetControllerCombinedState(
EnvironmentController controller, bool targetState, bool targetSoundState)
{
if (!TryGetControllerCombinedState(
controller,
Expand All @@ -693,7 +743,7 @@ private bool TrySetControllerCombinedState(EnvironmentController controller, boo
hasSound,
soundActive,
liveSoundActive,
targetState);
targetSoundState);
if (!needsWindowChange && !needsSoundChange)
{
return true;
Expand All @@ -705,7 +755,7 @@ private bool TrySetControllerCombinedState(EnvironmentController controller, boo
MethodInfo changeMuteMethod = needsSoundChange
? AccessTools.Method(
controller.GetType(),
targetState ? "MuteDeactivate" : "MuteActivate")
targetSoundState ? "MuteDeactivate" : "MuteActivate")
: null;
if ((needsWindowChange && changeWindowMethod == null) ||
(needsSoundChange && changeMuteMethod == null))
Expand Down Expand Up @@ -769,7 +819,11 @@ private bool TryGetControllerCombinedState(
FieldInfo ambientBehaviorField = AccessTools.Field(
controller.GetType(),
"_ambientSoundBehavior");
object ambientBehavior = ambientBehaviorField?.GetValue(controller);
if (ambientBehaviorField == null)
{
return false;
}
object ambientBehavior = ambientBehaviorField.GetValue(controller);
hasSound = ambientBehavior != null;
if (!hasSound)
{
Expand Down
3 changes: 2 additions & 1 deletion RealTimeWeatherMod/Core/WeatherDebugControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ internal static void StopManualPreview()
runtime.TryApplyBaseImmediately(_previewBase.Value, EnvironmentApplyMode.Interactive);
if (!ChillEnvPlugin.Cfg_EnableWeatherSync.Value)
foreach (var pair in _previewScenery)
runtime.TryApplySceneryState(pair.Key, pair.Value, EnvironmentApplyMode.Interactive);
runtime.TryApplySceneryState(pair.Key, pair.Value, EnvironmentApplyMode.Interactive,
applyPrecipitationSoundSetting: false);
}
_previewBase = null;
_previewScenery.Clear();
Expand Down
15 changes: 13 additions & 2 deletions RealTimeWeatherMod/Patches/ModSettingsIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ private bool TryRegisterSettings()
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_UI", "Show Weather on UI", "UIに天気を表示", "日期栏天气" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_DETAIL", "Detailed Segments", "詳細セグメント", "详细时段" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_EGG", "Seasonal Scenery", "季節の景色", "季节性景色" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_AMBIENT", "Ambient Sounds", "環境音", "环境音效" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_AMBIENT", "Ambient Sound Automation", "環境音の自動管理", "环境音效托管" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_PRECIPITATION_SOUND", "Precipitation Sounds (Weather Sync)", "降水音(天気同期)", "降水声音(天气同步)" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_RAIN_WINDOW", "Close Window in Rain", "雨天に窓を閉める", "下雨自动关窗" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_CITY", "City (Seniverse)", "都市(Seniverse)", "城市 (心知天气)" });
regTransMethod.Invoke(managerInstance, new object[] { "ENV_SYNC_PROVIDER", "Weather Provider", "天気提供元", "天气数据源" });
Expand Down Expand Up @@ -239,7 +240,7 @@ private bool TryRegisterSettings()
ChillEnvPlugin.Instance.Config.Save();
});

string labelAmbient = hasTranslation ? "ENV_SYNC_AMBIENT" : "环境音效";
string labelAmbient = hasTranslation ? "ENV_SYNC_AMBIENT" : "环境音效托管";
AddToggleSafe(managerInstance, managerType,
labelAmbient,
ChillEnvPlugin.Cfg_EnableAmbientSounds.Value,
Expand All @@ -249,6 +250,16 @@ private bool TryRegisterSettings()
ChillEnvPlugin.Instance.Config.Save();
});

AddToggleSafe(managerInstance, managerType,
hasTranslation ? "ENV_SYNC_PRECIPITATION_SOUND" : "降水声音(天气同步)",
ChillEnvPlugin.Cfg_EnablePrecipitationSounds.Value,
(value) =>
{
ChillEnvPlugin.Cfg_EnablePrecipitationSounds.Value = value;
ChillEnvPlugin.Instance.Config.Save();
Core.GameEnvironmentRuntime.Shared.RefreshPrecipitationSounds();
});

AddToggleSafe(managerInstance, managerType,
hasTranslation ? "ENV_SYNC_RAIN_WINDOW" : "下雨自动关窗",
ChillEnvPlugin.Cfg_CloseWindowWhenRaining.Value,
Expand Down
Loading