40250 classic: G1 integrate W4 (§9.1 map BGM selection, §9.2 background volume save/restore)
Doc conflicts resolved by W0: all four 增量 50 change-log bullets kept, W4 C.5 batch record appended. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
@@ -64,7 +64,13 @@ func _enter_background() -> void:
|
||||
if background_max_fps > 0:
|
||||
_saved_max_fps = Engine.max_fps
|
||||
Engine.max_fps = background_max_fps
|
||||
if _bound_audio and _bound_audio.has_method("stop_bgm"):
|
||||
# §9.2:不停 BGM,改为「保存音量 + 静音」(对标 CSoundManager::SaveVolume 后
|
||||
# SetMusicVolume(0))。_bgm_name 不清空,回前台恢复音量即可同一首继续播。
|
||||
if _bound_audio and _bound_audio.has_method("save_volume"):
|
||||
_bound_audio.save_volume()
|
||||
if _bound_audio.has_method("set_music_volume"):
|
||||
_bound_audio.set_music_volume(0.0)
|
||||
elif _bound_audio and _bound_audio.has_method("stop_bgm"):
|
||||
_bound_audio.stop_bgm()
|
||||
if _bound_client and _bound_client.has_method("suspend"):
|
||||
_bound_client.suspend()
|
||||
@@ -80,6 +86,9 @@ func _exit_background() -> void:
|
||||
get_tree().paused = false
|
||||
if background_max_fps > 0:
|
||||
Engine.max_fps = _saved_max_fps
|
||||
# §9.2:还原后台前保存的音量。
|
||||
if _bound_audio and _bound_audio.has_method("restore_volume"):
|
||||
_bound_audio.restore_volume()
|
||||
if _bound_client and _bound_client.has_method("resume"):
|
||||
_bound_client.resume()
|
||||
resumed.emit()
|
||||
|
||||
@@ -19,6 +19,12 @@ var master_bgm := 0.6
|
||||
var master_sfx := 0.9
|
||||
var _queued_bgm := ""
|
||||
|
||||
# §9.2 前后台音量存/恢复,字段名对齐参考 CSoundManager 的
|
||||
# m_fBackupMusicVolume / m_fBackupSoundVolume(REF/MilesLib/SoundManager.h:31)。
|
||||
var _backup_music_volume := -1.0
|
||||
var _backup_sound_volume := -1.0
|
||||
var _volume_saved := false
|
||||
|
||||
func _ready() -> void:
|
||||
if _queued_bgm != "":
|
||||
var n := _queued_bgm
|
||||
@@ -127,6 +133,32 @@ func _fade(pl: AudioStreamPlayer, to_db: float, dur: float, stop_after := false)
|
||||
if stop_after:
|
||||
tw.tween_callback(pl.stop)
|
||||
|
||||
# 立即把 master_bgm 应用到当前正在播放的 BGM 声道(无淡入)。
|
||||
# 对标 CSoundManager::SetMusicVolume:改的是「当前音乐实例」的音量。
|
||||
func set_music_volume(v: float) -> void:
|
||||
master_bgm = clampf(v, 0.0, 1.0)
|
||||
var pl: AudioStreamPlayer = _bgm[_bgm_cur]
|
||||
if pl and pl.playing:
|
||||
pl.volume_db = linear_to_db(master_bgm) if master_bgm > 0.0 else -80.0
|
||||
|
||||
# §9.2:进入后台前保存音量(CSoundManager::SaveVolume)。
|
||||
func save_volume() -> void:
|
||||
_backup_music_volume = master_bgm
|
||||
_backup_sound_volume = master_sfx
|
||||
_volume_saved = true
|
||||
|
||||
# §9.2:回到前台后恢复音量(CSoundManager::RestoreVolume)。
|
||||
# 不碰 _bgm_name / _bgm[],同一首继续播,只把音量拉回来。
|
||||
func restore_volume() -> void:
|
||||
if not _volume_saved:
|
||||
return
|
||||
master_bgm = _backup_music_volume
|
||||
master_sfx = _backup_sound_volume
|
||||
_volume_saved = false
|
||||
var pl: AudioStreamPlayer = _bgm[_bgm_cur]
|
||||
if pl and pl.playing:
|
||||
pl.volume_db = linear_to_db(master_bgm) if master_bgm > 0.0 else -80.0
|
||||
|
||||
# --- SFX -------------------------------------------------------------------
|
||||
|
||||
func play_ui(name: String) -> void:
|
||||
|
||||
+50
-8
@@ -1,8 +1,14 @@
|
||||
# BgmDirector —— 把 M2Client.bgm_changed 信号翻译成 Audio 的 BGM 播放。
|
||||
#
|
||||
# 存在的理由:§9.1 的地图 BGM 消费点,从 net_world.gd 里拆出来,避免 W2(可见性)
|
||||
# 和 W4(音频)同时改 net_world.gd。真正的「地图 → 曲目」解析和音量应用由 W4 在
|
||||
# 这里补全(见 docs/CLIENT-GAP-FIX.md §9.1)。
|
||||
# 和 W4(音频)同时改 net_world.gd。地图 → 曲目解析和音量应用在这里(§9.1)。
|
||||
#
|
||||
# 选曲逻辑对标 game.py:190(GameWindow.Open 内):
|
||||
# snd.SetMusicVolume(...); snd.SetSoundVolume(...) # 先应用音量
|
||||
# name = net.GetFieldMusicFileName()
|
||||
# if name: snd.FadeInMusic("BGM/" + name)
|
||||
# elif musicInfo.fieldMusic: snd.FadeInMusic("BGM/" + musicInfo.fieldMusic)
|
||||
# 默认音量 gs_fieldMusic_volume = 1.0f/5.0f*0.1f = 0.02f(服务器未下发时)。
|
||||
#
|
||||
# 用法:
|
||||
# var bd := preload("res://bgm_director.gd").new()
|
||||
@@ -10,6 +16,11 @@
|
||||
# bd.bind(client, audio)
|
||||
extends Node
|
||||
|
||||
const MusicInfo = preload("res://musicinfo.gd")
|
||||
|
||||
# gs_fieldMusic_volume 默认值(PythonNetworkStreamPhaseLoading.cpp:299)。
|
||||
const DEFAULT_FIELD_MUSIC_VOLUME := 1.0 / 5.0 * 0.1 # = 0.02
|
||||
|
||||
var _client: Node
|
||||
var _audio: Node
|
||||
|
||||
@@ -22,18 +33,49 @@ func bind(client: Node, audio: Node) -> void:
|
||||
_audio = audio
|
||||
if _client and _client.has_signal("bgm_changed"):
|
||||
_client.bgm_changed.connect(_on_bgm_changed)
|
||||
# 换图(§1.3):world_reset 后重跑选曲逻辑。新图的 BGM 包到达前沿用当前曲目。
|
||||
if _client and _client.has_signal("world_reset"):
|
||||
_client.world_reset.connect(_on_world_reset)
|
||||
|
||||
func _on_bgm_changed(name: String, volume: float) -> void:
|
||||
# M2Client 已在 EntityStore 侧做音量保留(BGM3 只带名、BGM4 带名+音量),
|
||||
# 这里拿到的 volume 就是「有效音量」,<0 仅表示从未收到过 BGM4。
|
||||
current_track = name
|
||||
if volume >= 0.0:
|
||||
current_volume = volume
|
||||
if _audio and "master_bgm" in _audio:
|
||||
_audio.master_bgm = clampf(volume, 0.0, 1.0)
|
||||
current_volume = volume
|
||||
_select()
|
||||
|
||||
func _on_world_reset() -> void:
|
||||
_select()
|
||||
|
||||
# game.py:190 的三行选曲 + 先置音量。
|
||||
func _select() -> void:
|
||||
if _audio == null:
|
||||
return
|
||||
if name == "":
|
||||
|
||||
# 1) 选曲:服务端名优先,否则回落 musicInfo.fieldMusic(METIN2THEMA = "M2BG.mp3")。
|
||||
var track := current_track
|
||||
if track == "":
|
||||
track = String(MusicInfo.fieldMusic)
|
||||
|
||||
# 2) 音量:服务端未指定(<0)用默认 0.02,否则用服务端值。
|
||||
var vol := current_volume if current_volume >= 0.0 else DEFAULT_FIELD_MUSIC_VOLUME
|
||||
vol = clampf(vol, 0.0, 1.0)
|
||||
|
||||
# 3) 先应用音量再淡入:Audio.play_bgm() 的淡入目标是 linear_to_db(master_bgm),
|
||||
# 对标 game.py 先 SetMusicVolume/SetSoundVolume 再 FadeInMusic。
|
||||
# 声音(SFX)音量在参考里取自系统设置(systemSetting.GetSoundVolume),
|
||||
# 不随 BGM 包下发,这里不改 master_sfx。
|
||||
if "master_bgm" in _audio:
|
||||
_audio.master_bgm = vol
|
||||
if _audio.has_method("set_music_volume"):
|
||||
_audio.set_music_volume(vol)
|
||||
|
||||
if track == "":
|
||||
if _audio.has_method("stop_bgm"):
|
||||
_audio.stop_bgm()
|
||||
return
|
||||
|
||||
if _audio.has_method("play_bgm"):
|
||||
_audio.play_bgm(name)
|
||||
_audio.play_bgm(track)
|
||||
# musicInfo.SaveLastPlayFieldMusic 语义:记录本次战场曲目。
|
||||
MusicInfo.save_last_play_field_music(track)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://yglonaagb03t
|
||||
@@ -0,0 +1,153 @@
|
||||
# bgm_test —— §9.1 地图 BGM 选曲 + §9.2 前后台音量存/恢复的 headless 自检。
|
||||
# godot --headless --path project --script bgm_test.gd
|
||||
# exit 0 = pass。
|
||||
extends SceneTree
|
||||
|
||||
const BgmDirector = preload("res://bgm_director.gd")
|
||||
const Audio = preload("res://audio.gd")
|
||||
const AppLifecycle = preload("res://app_lifecycle.gd")
|
||||
const MusicInfo = preload("res://musicinfo.gd")
|
||||
|
||||
const DEFAULT_VOL := 1.0 / 5.0 * 0.1 # 0.02
|
||||
|
||||
# 假 M2Client:只发 §9.1 关心的两个信号。
|
||||
class FakeClient extends Node:
|
||||
signal bgm_changed(name: String, volume: float)
|
||||
signal world_reset()
|
||||
|
||||
# 假 Audio:记录 play_bgm / stop_bgm 调用与音量,够 director 断言用。
|
||||
class FakeAudio extends Node:
|
||||
var master_bgm := 0.6
|
||||
var master_sfx := 0.9
|
||||
var _bgm_name := ""
|
||||
var played: Array[String] = []
|
||||
var stopped := 0
|
||||
func play_bgm(name: String, _fade := 1.2) -> void:
|
||||
if name == _bgm_name:
|
||||
return
|
||||
_bgm_name = name
|
||||
played.append(name)
|
||||
func stop_bgm(_fade := 1.0) -> void:
|
||||
_bgm_name = ""
|
||||
stopped += 1
|
||||
func set_music_volume(v: float) -> void:
|
||||
master_bgm = clampf(v, 0.0, 1.0)
|
||||
|
||||
var _fail := 0
|
||||
func _ck(c: bool, m: String) -> void:
|
||||
if not c:
|
||||
_fail += 1
|
||||
printerr("FAIL: " + m)
|
||||
|
||||
func _init() -> void:
|
||||
_test_select_server_volume()
|
||||
_test_select_default_volume()
|
||||
_test_select_empty_falls_back()
|
||||
_test_world_reset_reruns()
|
||||
_test_musicinfo_lastplay_roundtrip()
|
||||
_test_background_save_restore()
|
||||
if _fail == 0:
|
||||
print("PASS: bgm_test (§9.1 map BGM select + §9.2 volume save/restore)")
|
||||
quit(0)
|
||||
else:
|
||||
printerr("%d check(s) failed" % _fail)
|
||||
quit(1)
|
||||
|
||||
func _mk_director() -> Array:
|
||||
var client := FakeClient.new()
|
||||
var audio := FakeAudio.new()
|
||||
var bd := BgmDirector.new()
|
||||
get_root().add_child(client)
|
||||
get_root().add_child(audio)
|
||||
get_root().add_child(bd)
|
||||
bd.bind(client, audio)
|
||||
return [client, audio, bd]
|
||||
|
||||
# §9.1:服务端给了名字 + 音量 -> 用服务端音量并播放该曲。
|
||||
func _test_select_server_volume() -> void:
|
||||
var t := _mk_director()
|
||||
var client: FakeClient = t[0]
|
||||
var audio: FakeAudio = t[1]
|
||||
client.bgm_changed.emit("pvp_bgm.mp3", 0.5)
|
||||
_ck(audio.played == ["pvp_bgm.mp3"], "server name played, got %s" % [audio.played])
|
||||
_ck(is_equal_approx(audio.master_bgm, 0.5), "server volume applied, got %f" % audio.master_bgm)
|
||||
_ck(audio.stopped == 0, "no stop_bgm on server-provided track")
|
||||
t[0].free(); t[1].free(); t[2].free()
|
||||
|
||||
# §9.1:服务端给了名字但音量 <0(GC_MAIN_CHARACTER3_BGM,未收 BGM4)-> 默认 0.02。
|
||||
func _test_select_default_volume() -> void:
|
||||
var t := _mk_director()
|
||||
var client: FakeClient = t[0]
|
||||
var audio: FakeAudio = t[1]
|
||||
client.bgm_changed.emit("field_a.mp3", -1.0)
|
||||
_ck(audio.played == ["field_a.mp3"], "name played on unspecified volume, got %s" % [audio.played])
|
||||
_ck(is_equal_approx(audio.master_bgm, DEFAULT_VOL), "default 0.02 applied, got %f" % audio.master_bgm)
|
||||
t[0].free(); t[1].free(); t[2].free()
|
||||
|
||||
# §9.1:服务端名为空 -> 回落 musicInfo.fieldMusic(METIN2THEMA = "M2BG.mp3")。
|
||||
func _test_select_empty_falls_back() -> void:
|
||||
var t := _mk_director()
|
||||
var client: FakeClient = t[0]
|
||||
var audio: FakeAudio = t[1]
|
||||
client.bgm_changed.emit("", -1.0)
|
||||
_ck(audio.played == [String(MusicInfo.fieldMusic)],
|
||||
"empty name falls back to fieldMusic, got %s" % [audio.played])
|
||||
_ck(String(MusicInfo.fieldMusic) == "M2BG.mp3", "METIN2THEMA constant is M2BG.mp3")
|
||||
_ck(is_equal_approx(audio.master_bgm, DEFAULT_VOL), "default 0.02 on fallback, got %f" % audio.master_bgm)
|
||||
t[0].free(); t[1].free(); t[2].free()
|
||||
|
||||
# §9.1 / §1.3:换图(world_reset)重跑选曲逻辑。
|
||||
func _test_world_reset_reruns() -> void:
|
||||
var t := _mk_director()
|
||||
var client: FakeClient = t[0]
|
||||
var audio: FakeAudio = t[1]
|
||||
var bd: BgmDirector = t[2]
|
||||
_ck(client.world_reset.is_connected(bd._on_world_reset), "director connected to world_reset")
|
||||
client.bgm_changed.emit("map_a.mp3", 0.4)
|
||||
# warp:world_reset 先到(旧曲目仍在 store 里),随后新图的 BGM 包。
|
||||
client.world_reset.emit()
|
||||
_ck(audio.played == ["map_a.mp3"], "world_reset keeps current track, got %s" % [audio.played])
|
||||
client.bgm_changed.emit("map_b.mp3", 0.2)
|
||||
_ck(audio.played == ["map_a.mp3", "map_b.mp3"], "new map BGM played, got %s" % [audio.played])
|
||||
_ck(is_equal_approx(audio.master_bgm, 0.2), "new map volume applied, got %f" % audio.master_bgm)
|
||||
t[0].free(); t[1].free(); t[2].free()
|
||||
|
||||
# §9.1:musicInfo.SaveLastPlayFieldMusic / LoadLastPlayFieldMusic 往返。
|
||||
func _test_musicinfo_lastplay_roundtrip() -> void:
|
||||
MusicInfo.save_last_play_field_music("last_field.mp3")
|
||||
_ck(MusicInfo.load_last_play_field_music() == "last_field.mp3",
|
||||
"lastplay.inf roundtrip, got %s" % MusicInfo.load_last_play_field_music())
|
||||
|
||||
# §9.2:后台保存音量 + 静音,前台恢复;_bgm_name 不变,同一首继续播。
|
||||
func _test_background_save_restore() -> void:
|
||||
var client := FakeClient.new()
|
||||
var audio := Audio.new()
|
||||
var life := AppLifecycle.new()
|
||||
get_root().add_child(audio)
|
||||
audio.setup("res://__no_assets__") # 无资源,play_bgm 只置名不出声
|
||||
get_root().add_child(life)
|
||||
life.pause_tree_on_background = false # 别把测试树暂停了
|
||||
life.background_max_fps = 0
|
||||
life.bind(null, audio)
|
||||
|
||||
audio.master_bgm = 0.6
|
||||
audio.master_sfx = 0.9
|
||||
# 模拟「某首战场曲目正在播放」(headless 无资源,不实际解码)。
|
||||
audio._bgm_name = "M2BG.mp3"
|
||||
var name_before := audio._bgm_name
|
||||
_ck(name_before == "M2BG.mp3", "bgm name set before background, got %s" % name_before)
|
||||
|
||||
life._enter_background()
|
||||
_ck(is_equal_approx(audio.master_bgm, 0.0), "music muted in background, got %f" % audio.master_bgm)
|
||||
_ck(audio._volume_saved, "save_volume ran (backup taken)")
|
||||
_ck(is_equal_approx(audio._backup_music_volume, 0.6), "backup music volume, got %f" % audio._backup_music_volume)
|
||||
_ck(is_equal_approx(audio._backup_sound_volume, 0.9), "backup sound volume, got %f" % audio._backup_sound_volume)
|
||||
_ck(audio._bgm_name == name_before, "_bgm_name unchanged in background, got %s" % audio._bgm_name)
|
||||
|
||||
life._exit_background()
|
||||
_ck(is_equal_approx(audio.master_bgm, 0.6), "music volume restored, got %f" % audio.master_bgm)
|
||||
_ck(is_equal_approx(audio.master_sfx, 0.9), "sound volume restored, got %f" % audio.master_sfx)
|
||||
_ck(audio._bgm_name == name_before, "_bgm_name still unchanged after restore, got %s" % audio._bgm_name)
|
||||
_ck(not audio._volume_saved, "backup flag cleared after restore")
|
||||
|
||||
client.free(); life.free(); audio.free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxhy4wk3451c1
|
||||
@@ -0,0 +1,47 @@
|
||||
# MusicInfo —— 对标 POC/assets/root/musicinfo.py:默认曲目常量 + 上次战场曲目持久化。
|
||||
#
|
||||
# 参考(musicinfo.py):
|
||||
# METIN2THEMA = "M2BG.mp3"
|
||||
# loginMusic = "login_window.mp3"
|
||||
# createMusic = "characterselect.mp3"
|
||||
# selectMusic = "characterselect.mp3"
|
||||
# fieldMusic = METIN2THEMA
|
||||
# SaveLastPlayFieldMusic() / LoadLastPlayFieldMusic() → 文件 "BGM/lastplay.inf"
|
||||
#
|
||||
# 用法:
|
||||
# const MusicInfo = preload("res://musicinfo.gd")
|
||||
# MusicInfo.fieldMusic # -> "M2BG.mp3"
|
||||
# MusicInfo.save_last_play_field_music("x.mp3")
|
||||
# MusicInfo.load_last_play_field_music() # -> "x.mp3" 或 ""
|
||||
extends RefCounted
|
||||
|
||||
const METIN2THEMA := "M2BG.mp3"
|
||||
const loginMusic := "login_window.mp3"
|
||||
const createMusic := "characterselect.mp3"
|
||||
const selectMusic := "characterselect.mp3" # 选人页
|
||||
const fieldMusic := METIN2THEMA
|
||||
|
||||
# 参考写死 "BGM/lastplay.inf"(相对客户端目录);Godot 侧落到用户可写目录。
|
||||
const LAST_PLAY_DIR := "user://BGM"
|
||||
const LAST_PLAY_PATH := "user://BGM/lastplay.inf"
|
||||
|
||||
# musicInfo.SaveLastPlayFieldMusic(name)
|
||||
static func save_last_play_field_music(name: String) -> void:
|
||||
if name == "":
|
||||
return
|
||||
DirAccess.make_dir_recursive_absolute(LAST_PLAY_DIR)
|
||||
var f := FileAccess.open(LAST_PLAY_PATH, FileAccess.WRITE)
|
||||
if f:
|
||||
f.store_string(name)
|
||||
f.close()
|
||||
|
||||
# musicInfo.LoadLastPlayFieldMusic() -> 曲目名(无记录时空串)
|
||||
static func load_last_play_field_music() -> String:
|
||||
if not FileAccess.file_exists(LAST_PLAY_PATH):
|
||||
return ""
|
||||
var f := FileAccess.open(LAST_PLAY_PATH, FileAccess.READ)
|
||||
if f == null:
|
||||
return ""
|
||||
var s := f.get_as_text()
|
||||
f.close()
|
||||
return s.strip_edges()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ektx487ombx1
|
||||
Reference in New Issue
Block a user