fix(chat): support body and content fields in world and DM chat messages; feat(audio): add sound toggle switch and mute controls
checkpoint / checkpoint (push) Successful in 6m11s
checkpoint / checkpoint (push) Successful in 6m11s
This commit is contained in:
@@ -71,7 +71,7 @@ from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -97,7 +97,33 @@ _BODY_MAX_LEN = 4000
|
||||
|
||||
|
||||
class SendDMRequest(BaseModel):
|
||||
body: str = Field(..., min_length=1, max_length=_BODY_MAX_LEN)
|
||||
body: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
content: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def resolve_body_or_content(cls, data: object) -> object:
|
||||
if isinstance(data, dict):
|
||||
if not data.get("body") and data.get("content"):
|
||||
data["body"] = data["content"]
|
||||
elif not data.get("content") and data.get("body"):
|
||||
data["content"] = data["body"]
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_non_empty(self) -> SendDMRequest:
|
||||
text = self.body or self.content
|
||||
if not text or len(text.strip()) < 1:
|
||||
raise ValueError("Message body or content must not be empty")
|
||||
if not self.body:
|
||||
self.body = text
|
||||
if not self.content:
|
||||
self.content = text
|
||||
return self
|
||||
|
||||
|
||||
class StartConversationRequest(BaseModel):
|
||||
target_user_id: int
|
||||
|
||||
|
||||
class DMMessageResponse(BaseModel):
|
||||
@@ -111,6 +137,11 @@ class DMMessageResponse(BaseModel):
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def content(self) -> str:
|
||||
return self.body
|
||||
|
||||
|
||||
class DMMessageListResponse(BaseModel):
|
||||
messages: list[DMMessageResponse]
|
||||
@@ -397,3 +428,65 @@ async def list_conversations(
|
||||
)
|
||||
|
||||
return DMConversationListResponse(conversations=summaries, total=total)
|
||||
@router.post("/conversations")
|
||||
async def start_or_get_conversation_endpoint(
|
||||
body: StartConversationRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
):
|
||||
"""Start or get existing conversation with target_user_id."""
|
||||
if body.target_user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot start a conversation with yourself"
|
||||
)
|
||||
await _get_target_user(db, body.target_user_id)
|
||||
conversation = await _get_or_create_conversation(db, current_user.id, body.target_user_id)
|
||||
return {"id": conversation.id, "target_user_id": body.target_user_id}
|
||||
|
||||
|
||||
@router.get("/conversations/{conversation_id}/messages", response_model=DMMessageListResponse)
|
||||
async def get_dm_conversation_history(
|
||||
conversation_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
before: int | None = Query(None),
|
||||
):
|
||||
"""Get messages for a conversation by conversation_id."""
|
||||
conv_result = await db.execute(
|
||||
select(Conversation).where(
|
||||
Conversation.id == conversation_id,
|
||||
or_(Conversation.user_a_id == current_user.id, Conversation.user_b_id == current_user.id),
|
||||
)
|
||||
)
|
||||
conversation = conv_result.scalar_one_or_none()
|
||||
if conversation is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found")
|
||||
peer_id = _peer_id(conversation, current_user.id)
|
||||
return await get_dm_history(peer_id, current_user, db, limit=limit, before=before)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/conversations/{conversation_id}/messages",
|
||||
response_model=DMMessageResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(chat_dm_send_rate_limit)],
|
||||
)
|
||||
async def send_dm_to_conversation(
|
||||
conversation_id: int,
|
||||
body: SendDMRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
):
|
||||
"""Send a message to a conversation by conversation_id."""
|
||||
conv_result = await db.execute(
|
||||
select(Conversation).where(
|
||||
Conversation.id == conversation_id,
|
||||
or_(Conversation.user_a_id == current_user.id, Conversation.user_b_id == current_user.id),
|
||||
)
|
||||
)
|
||||
conversation = conv_result.scalar_one_or_none()
|
||||
if conversation is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found")
|
||||
peer_id = _peer_id(conversation, current_user.id)
|
||||
return await send_dm(peer_id, body, current_user, db)
|
||||
|
||||
@@ -39,7 +39,7 @@ from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
from sqlalchemy import and_, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
@@ -68,9 +68,31 @@ _BODY_MAX_LEN = 4000
|
||||
|
||||
|
||||
class SendWorldMessageRequest(BaseModel):
|
||||
body: str = Field(..., min_length=1, max_length=_BODY_MAX_LEN)
|
||||
body: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
content: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
room_id: int | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def resolve_body_or_content(cls, data: object) -> object:
|
||||
if isinstance(data, dict):
|
||||
if not data.get("body") and data.get("content"):
|
||||
data["body"] = data["content"]
|
||||
elif not data.get("content") and data.get("body"):
|
||||
data["content"] = data["body"]
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_non_empty(self) -> SendWorldMessageRequest:
|
||||
text = self.body or self.content
|
||||
if not text or len(text.strip()) < 1:
|
||||
raise ValueError("Message body or content must not be empty")
|
||||
if not self.body:
|
||||
self.body = text
|
||||
if not self.content:
|
||||
self.content = text
|
||||
return self
|
||||
|
||||
|
||||
class AuthorInfo(BaseModel):
|
||||
id: int | None
|
||||
@@ -92,6 +114,11 @@ class WorldMessageResponse(BaseModel):
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def content(self) -> str:
|
||||
return self.body
|
||||
|
||||
|
||||
class WorldMessageListResponse(BaseModel):
|
||||
messages: list[WorldMessageResponse]
|
||||
|
||||
+22
-1
@@ -195,7 +195,28 @@ func request(
|
||||
var detail := "HTTP error %d" % response_code
|
||||
var parsed: Variant = _parse_json(json_string)
|
||||
if parsed is Dictionary and (parsed as Dictionary).has("detail"):
|
||||
detail = str((parsed as Dictionary)["detail"])
|
||||
var raw_detail = (parsed as Dictionary)["detail"]
|
||||
if raw_detail is Array and not raw_detail.is_empty():
|
||||
var parts: Array[String] = []
|
||||
for val_err in raw_detail:
|
||||
if val_err is Dictionary and val_err.has("msg"):
|
||||
var loc_arr = val_err.get("loc", [])
|
||||
var loc_str := ""
|
||||
if loc_arr is Array:
|
||||
var filtered_loc: Array[String] = []
|
||||
for x in loc_arr:
|
||||
if str(x) != "body":
|
||||
filtered_loc.append(str(x))
|
||||
loc_str = ".".join(filtered_loc)
|
||||
if loc_str != "":
|
||||
parts.append("%s: %s" % [loc_str, str(val_err.get("msg"))])
|
||||
else:
|
||||
parts.append(str(val_err.get("msg")))
|
||||
else:
|
||||
parts.append(str(val_err))
|
||||
detail = ", ".join(parts)
|
||||
else:
|
||||
detail = str(raw_detail)
|
||||
return _make_result(false, response_code, null, detail)
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,16 @@ const STREAM_BELL: AudioStream = preload("res://assets/audio/bell.wav")
|
||||
const STREAM_DOOR: AudioStream = preload("res://assets/audio/door_open.wav")
|
||||
|
||||
const POOL_SIZE: int = 16
|
||||
const SETTINGS_FILE := "user://audio_settings.json"
|
||||
|
||||
signal sound_toggled(enabled: bool)
|
||||
|
||||
var _players: Array[AudioStreamPlayer] = []
|
||||
var _player_index: int = 0
|
||||
|
||||
## Whether sound effects and audio output are enabled.
|
||||
var sound_enabled: bool = true
|
||||
|
||||
## Number of active AudioStreamPlayers in the pool.
|
||||
var pool_size: int:
|
||||
get:
|
||||
@@ -26,12 +32,59 @@ var pool_size: int:
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_init_pool()
|
||||
_load_settings()
|
||||
var tree := get_tree()
|
||||
if tree != null:
|
||||
tree.node_added.connect(_on_node_added)
|
||||
_hook_existing_buttons(tree.root)
|
||||
|
||||
|
||||
func _load_settings() -> void:
|
||||
if FileAccess.file_exists(SETTINGS_FILE):
|
||||
var file := FileAccess.open(SETTINGS_FILE, FileAccess.READ)
|
||||
if file != null:
|
||||
var text := file.get_as_text()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if parsed is Dictionary and parsed.has("sound_enabled"):
|
||||
set_sound_enabled(bool(parsed["sound_enabled"]), false)
|
||||
return
|
||||
set_sound_enabled(true, false)
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
var file := FileAccess.open(SETTINGS_FILE, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify({"sound_enabled": sound_enabled}))
|
||||
|
||||
|
||||
## Check whether sound is currently enabled.
|
||||
func is_sound_enabled() -> bool:
|
||||
return sound_enabled
|
||||
|
||||
|
||||
## Enable or disable sound output.
|
||||
func set_sound_enabled(enabled: bool, save: bool = true) -> void:
|
||||
sound_enabled = enabled
|
||||
if not sound_enabled:
|
||||
for player in _players:
|
||||
if player != null and is_instance_valid(player) and player.playing:
|
||||
player.stop()
|
||||
var master_idx := AudioServer.get_bus_index("Master")
|
||||
if master_idx >= 0:
|
||||
AudioServer.set_bus_mute(master_idx, not sound_enabled)
|
||||
if save:
|
||||
_save_settings()
|
||||
sound_toggled.emit(sound_enabled)
|
||||
|
||||
|
||||
## Toggle sound between ON and OFF. Returns new state.
|
||||
func toggle_sound() -> bool:
|
||||
set_sound_enabled(not sound_enabled)
|
||||
if sound_enabled:
|
||||
play_click()
|
||||
return sound_enabled
|
||||
|
||||
|
||||
func _init_pool() -> void:
|
||||
if not _players.is_empty():
|
||||
return
|
||||
@@ -45,6 +98,8 @@ func _init_pool() -> void:
|
||||
|
||||
## Play an audio stream on the specified bus with volume and pitch settings.
|
||||
func play_sound(stream: AudioStream, bus: String = "Master", volume_db: float = 0.0, pitch_scale: float = 1.0) -> AudioStreamPlayer:
|
||||
if not sound_enabled:
|
||||
return null
|
||||
if stream == null:
|
||||
return null
|
||||
if _players.is_empty():
|
||||
@@ -120,6 +175,8 @@ func _hook_button(btn: BaseButton) -> void:
|
||||
|
||||
|
||||
func _on_button_pressed(btn: BaseButton) -> void:
|
||||
if not sound_enabled:
|
||||
return
|
||||
if not is_instance_valid(btn):
|
||||
return
|
||||
if btn.is_in_group("no_click_sound"):
|
||||
|
||||
+1
-1
@@ -280,7 +280,7 @@ func _add_message_row(container: VBoxContainer, msg_data: Dictionary, conv_type:
|
||||
elif msg_data.has("sender") and msg_data["sender"] is Dictionary:
|
||||
author = str(msg_data["sender"].get("username", author))
|
||||
|
||||
var content: String = str(msg_data.get("content", ""))
|
||||
var content: String = str(msg_data.get("content", msg_data.get("body", "")))
|
||||
|
||||
var label := Label.new()
|
||||
label.text = "[%s]: %s" % [author, content]
|
||||
|
||||
@@ -13,6 +13,7 @@ const CONTROLS_DATA = [
|
||||
{"key": "E", "action": "Interact with objects & chests"},
|
||||
{"key": "T / Enter", "action": "Open In-World Chat"},
|
||||
{"key": "Tab / F1", "action": "Friends & Social Panel"},
|
||||
{"key": "M", "action": "Toggle Sound (Mute / Unmute)"},
|
||||
{"key": "F2", "action": "Creator Studio / World Editor"},
|
||||
{"key": "F11 / Alt+Enter", "action": "Toggle Fullscreen"},
|
||||
{"key": "ESC", "action": "Pause Menu / Go Back / Close Modal"},
|
||||
|
||||
+41
-1
@@ -188,6 +188,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
_toggle_friends_overlay()
|
||||
return
|
||||
elif event.keycode == KEY_M:
|
||||
if not (_build_editor != null and is_instance_valid(_build_editor)) and not (_chat_overlay != null and is_instance_valid(_chat_overlay)):
|
||||
get_viewport().set_input_as_handled()
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
||||
audio_mgr.toggle_sound()
|
||||
return
|
||||
|
||||
if event.is_action_pressed("toggle_build_mode"):
|
||||
get_viewport().set_input_as_handled()
|
||||
@@ -263,6 +270,27 @@ func _setup_in_game_hud() -> void:
|
||||
report_btn.pressed.connect(_toggle_report_modal)
|
||||
bar.add_child(report_btn)
|
||||
|
||||
var sound_btn := Button.new()
|
||||
sound_btn.name = "SoundButton"
|
||||
_update_hud_sound_button(sound_btn)
|
||||
sound_btn.pressed.connect(func():
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
||||
audio_mgr.toggle_sound()
|
||||
_update_hud_sound_button(sound_btn)
|
||||
)
|
||||
var audio_singleton = get_node_or_null("/root/AudioManager")
|
||||
if audio_singleton and audio_singleton.has_signal("sound_toggled"):
|
||||
var on_toggled = func(_enabled: bool):
|
||||
if is_instance_valid(sound_btn):
|
||||
_update_hud_sound_button(sound_btn)
|
||||
audio_singleton.sound_toggled.connect(on_toggled)
|
||||
sound_btn.tree_exiting.connect(func():
|
||||
if is_instance_valid(audio_singleton) and audio_singleton.sound_toggled.is_connected(on_toggled):
|
||||
audio_singleton.sound_toggled.disconnect(on_toggled)
|
||||
)
|
||||
bar.add_child(sound_btn)
|
||||
|
||||
var exit_btn := Button.new()
|
||||
exit_btn.text = "🚪 Menu [ESC]"
|
||||
exit_btn.pressed.connect(func():
|
||||
@@ -301,7 +329,7 @@ func _setup_in_game_hud() -> void:
|
||||
controls_pill.offset_right = -220
|
||||
|
||||
var hint_lbl := Label.new()
|
||||
hint_lbl.text = "[W][A][S][D] Move [Space] Jump [E] Interact [F1] Friends [T] Chat [F2] Build [ESC] Menu"
|
||||
hint_lbl.text = "[W][A][S][D] Move [Space] Jump [E] Interact [F1] Friends [T] Chat [M] Sound [F2] Build [ESC] Menu"
|
||||
hint_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
hint_lbl.add_theme_font_size_override("font_size", 12)
|
||||
hint_lbl.add_theme_color_override("font_color", Color(0.85, 0.9, 0.95, 0.85))
|
||||
@@ -336,6 +364,18 @@ func _setup_in_game_hud() -> void:
|
||||
_hud_layer.add_child(creator_pill)
|
||||
|
||||
|
||||
static func _update_hud_sound_button(btn: Button) -> void:
|
||||
if btn == null or not is_instance_valid(btn):
|
||||
return
|
||||
var main_loop = Engine.get_main_loop()
|
||||
var audio_mgr: Node = main_loop.root.get_node_or_null("AudioManager") if main_loop is SceneTree else null
|
||||
var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true
|
||||
if enabled:
|
||||
btn.text = "🔊 Sound [M]"
|
||||
else:
|
||||
btn.text = "🔇 Muted [M]"
|
||||
|
||||
|
||||
func _toggle_report_modal() -> void:
|
||||
if _report_modal != null and is_instance_valid(_report_modal):
|
||||
_close_report_modal()
|
||||
|
||||
@@ -87,6 +87,18 @@ static func _update_fs_button(btn: Button) -> void:
|
||||
btn.text = "⛶ Fullscreen (F11)"
|
||||
|
||||
|
||||
static func _update_sound_button(btn: Button) -> void:
|
||||
if btn == null or not is_instance_valid(btn):
|
||||
return
|
||||
var main_loop = Engine.get_main_loop()
|
||||
var audio_mgr: Node = main_loop.root.get_node_or_null("AudioManager") if main_loop is SceneTree else null
|
||||
var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true
|
||||
if enabled:
|
||||
btn.text = "🔊 Sound: ON"
|
||||
else:
|
||||
btn.text = "🔇 Sound: OFF"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var v := Label.new()
|
||||
var bl := _build_label()
|
||||
@@ -134,6 +146,33 @@ func _ready() -> void:
|
||||
ctrl_btn.pressed.connect(_on_controls_button_pressed)
|
||||
add_child(ctrl_btn)
|
||||
|
||||
var sound_btn := Button.new()
|
||||
sound_btn.name = "SoundButton"
|
||||
sound_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
sound_btn.offset_left = -440
|
||||
sound_btn.offset_top = 10
|
||||
sound_btn.offset_right = -310
|
||||
sound_btn.offset_bottom = 38
|
||||
sound_btn.add_theme_font_size_override("font_size", 12)
|
||||
_update_sound_button(sound_btn)
|
||||
sound_btn.pressed.connect(func():
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
||||
audio_mgr.toggle_sound()
|
||||
_update_sound_button(sound_btn)
|
||||
)
|
||||
var audio_singleton = get_node_or_null("/root/AudioManager")
|
||||
if audio_singleton and audio_singleton.has_signal("sound_toggled"):
|
||||
var on_toggled = func(_enabled: bool):
|
||||
if is_instance_valid(sound_btn):
|
||||
_update_sound_button(sound_btn)
|
||||
audio_singleton.sound_toggled.connect(on_toggled)
|
||||
sound_btn.tree_exiting.connect(func():
|
||||
if is_instance_valid(audio_singleton) and audio_singleton.sound_toggled.is_connected(on_toggled):
|
||||
audio_singleton.sound_toggled.disconnect(on_toggled)
|
||||
)
|
||||
add_child(sound_btn)
|
||||
|
||||
# Cache node references.
|
||||
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
|
||||
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
|
||||
|
||||
+37
-1
@@ -17,6 +17,7 @@ var world_title: String = "World"
|
||||
var _confirm_box: VBoxContainer
|
||||
var _main_menu_vbox: VBoxContainer
|
||||
var _fs_button: Button
|
||||
var _sound_button: Button
|
||||
|
||||
|
||||
func _init(p_can_edit: bool = false, p_world_title: String = "World") -> void:
|
||||
@@ -101,6 +102,21 @@ func _ready() -> void:
|
||||
_update_fs_label()
|
||||
_main_menu_vbox.add_child(_fs_button)
|
||||
|
||||
_sound_button = _make_menu_btn("🔊 Sound FX: ON (M)", _on_toggle_sound)
|
||||
_update_sound_label()
|
||||
_main_menu_vbox.add_child(_sound_button)
|
||||
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
if audio_mgr and audio_mgr.has_signal("sound_toggled"):
|
||||
var on_toggled = func(_enabled: bool):
|
||||
if is_instance_valid(_sound_button):
|
||||
_update_sound_label()
|
||||
audio_mgr.sound_toggled.connect(on_toggled)
|
||||
_sound_button.tree_exiting.connect(func():
|
||||
if is_instance_valid(audio_mgr) and audio_mgr.sound_toggled.is_connected(on_toggled):
|
||||
audio_mgr.sound_toggled.disconnect(on_toggled)
|
||||
)
|
||||
|
||||
var leave_btn := _make_menu_btn("🚪 Leave World", _on_prompt_leave, Color(1.0, 0.45, 0.45))
|
||||
_main_menu_vbox.add_child(leave_btn)
|
||||
|
||||
@@ -152,7 +168,9 @@ func _on_resume_pressed() -> void:
|
||||
|
||||
|
||||
func _on_toggle_fs() -> void:
|
||||
Session.toggle_fullscreen()
|
||||
var sess = Engine.get_main_loop().root.get_node_or_null("Session") if Engine.get_main_loop() else null
|
||||
if sess and sess.has_method("toggle_fullscreen"):
|
||||
sess.toggle_fullscreen()
|
||||
_update_fs_label()
|
||||
|
||||
|
||||
@@ -169,6 +187,24 @@ func _update_fs_label() -> void:
|
||||
_fs_button.text = "⛶ Fullscreen Mode (F11)"
|
||||
|
||||
|
||||
func _on_toggle_sound() -> void:
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
||||
audio_mgr.toggle_sound()
|
||||
_update_sound_label()
|
||||
|
||||
|
||||
func _update_sound_label() -> void:
|
||||
if _sound_button == null or not is_instance_valid(_sound_button):
|
||||
return
|
||||
var audio_mgr = get_node_or_null("/root/AudioManager")
|
||||
var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true
|
||||
if enabled:
|
||||
_sound_button.text = "🔊 Sound FX: ON (M)"
|
||||
else:
|
||||
_sound_button.text = "🔇 Sound FX: OFF (M)"
|
||||
|
||||
|
||||
func _on_prompt_leave() -> void:
|
||||
_main_menu_vbox.visible = false
|
||||
_confirm_box.visible = true
|
||||
|
||||
@@ -15,6 +15,7 @@ func _run() -> void:
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: AudioManager autoload exists in scene tree.")
|
||||
audio_mgr.set_sound_enabled(true, false)
|
||||
|
||||
# 1. Verify streams are valid AudioStream objects
|
||||
var streams := [
|
||||
@@ -117,9 +118,50 @@ func _run() -> void:
|
||||
# Button in no_click_sound group
|
||||
var silent_btn := Button.new()
|
||||
silent_btn.add_to_group("no_click_sound")
|
||||
root.add_child(silent_btn)
|
||||
await process_frame
|
||||
silent_btn.emit_signal("pressed")
|
||||
# 6. Verify sound toggle & mute functionality
|
||||
var tracker := {"received": false, "val": false}
|
||||
audio_mgr.sound_toggled.connect(func(val: bool):
|
||||
tracker["received"] = true
|
||||
tracker["val"] = val
|
||||
)
|
||||
|
||||
# Toggle OFF
|
||||
var new_state: bool = audio_mgr.toggle_sound()
|
||||
if new_state != false or audio_mgr.is_sound_enabled() != false:
|
||||
printerr("FAIL: toggle_sound() should disable sound")
|
||||
quit(1)
|
||||
return
|
||||
if not tracker["received"] or tracker["val"] != false:
|
||||
printerr("FAIL: sound_toggled signal not fired on disable")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# When sound disabled, play_click should return null
|
||||
var disabled_p: AudioStreamPlayer = audio_mgr.play_click()
|
||||
if disabled_p != null:
|
||||
printerr("FAIL: play_click() returned non-null when sound disabled")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Sound disable successfully suppressed audio playback.")
|
||||
|
||||
# Toggle back ON
|
||||
tracker["received"] = false
|
||||
new_state = audio_mgr.toggle_sound()
|
||||
if new_state != true or audio_mgr.is_sound_enabled() != true:
|
||||
printerr("FAIL: toggle_sound() should re-enable sound")
|
||||
quit(1)
|
||||
return
|
||||
if not tracker["received"] or tracker["val"] != true:
|
||||
printerr("FAIL: sound_toggled signal not fired on re-enable")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var reenabled_p: AudioStreamPlayer = audio_mgr.play_click()
|
||||
if reenabled_p == null:
|
||||
printerr("FAIL: play_click() returned null after re-enabling sound")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Sound re-enable restored audio playback and signal notifications.")
|
||||
|
||||
print("SMOKE RESULT: PASS")
|
||||
quit(0)
|
||||
|
||||
@@ -58,6 +58,14 @@ func _run() -> void:
|
||||
}
|
||||
ui._add_message_row(ui.world_messages_container, mock_msg, "world")
|
||||
|
||||
# Test message row with body field fallback
|
||||
var mock_msg_body := {
|
||||
"id": 102,
|
||||
"author": {"username": "BodyUser"},
|
||||
"body": "Fallback body content."
|
||||
}
|
||||
ui._add_message_row(ui.world_messages_container, mock_msg_body, "world")
|
||||
|
||||
if ui.world_messages_container.get_child_count() == 0:
|
||||
printerr("FAIL: Message row was not added to world_messages_container")
|
||||
quit(1)
|
||||
@@ -75,6 +83,14 @@ func _run() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var row2: HBoxContainer = ui.world_messages_container.get_child(1)
|
||||
var lbl2: Label = row2.get_child(0) as Label
|
||||
if not lbl2 or not lbl2.text.contains("Fallback body content."):
|
||||
printerr("FAIL: Fallback body text was not displayed in message row")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Message row with body fallback verified.")
|
||||
|
||||
print("PASS: Message row with Report button verified.")
|
||||
|
||||
# Test report dialog preparation
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
extends SceneTree
|
||||
## Headless verification for world chat message send and sound toggle.
|
||||
|
||||
const PauseMenuScript = preload("res://scripts/pause_menu.gd")
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
print("SMOKE: Starting World Chat Send & Sound Toggle verification...")
|
||||
|
||||
# 1. Verify AudioManager and sound toggle
|
||||
var audio_mgr = root.get_node_or_null("AudioManager")
|
||||
if audio_mgr == null:
|
||||
printerr("FAIL: AudioManager not found in root")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Test initial state
|
||||
audio_mgr.set_sound_enabled(true, false)
|
||||
if not audio_mgr.is_sound_enabled():
|
||||
printerr("FAIL: sound_enabled should be true")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var p = audio_mgr.play_click()
|
||||
if p == null:
|
||||
printerr("FAIL: play_click should return player when sound enabled")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Sound plays when enabled.")
|
||||
|
||||
# Test toggle OFF
|
||||
var toggled: bool = audio_mgr.toggle_sound()
|
||||
if toggled != false or audio_mgr.is_sound_enabled() != false:
|
||||
printerr("FAIL: toggle_sound should return false")
|
||||
quit(1)
|
||||
return
|
||||
if audio_mgr.play_click() != null:
|
||||
printerr("FAIL: play_click should return null when sound disabled")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Sound disabled and suppressed.")
|
||||
|
||||
# Test toggle ON
|
||||
toggled = audio_mgr.toggle_sound()
|
||||
if toggled != true or audio_mgr.is_sound_enabled() != true:
|
||||
printerr("FAIL: toggle_sound should return true")
|
||||
quit(1)
|
||||
return
|
||||
if audio_mgr.play_click() == null:
|
||||
printerr("FAIL: play_click should return player after re-enabling")
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: Sound re-enabled and restored.")
|
||||
|
||||
# 2. Verify MainMenu SoundButton
|
||||
var main_menu_scene := load("res://scenes/main_menu.tscn")
|
||||
if main_menu_scene:
|
||||
var menu = main_menu_scene.instantiate()
|
||||
root.add_child(menu)
|
||||
await process_frame
|
||||
var sound_btn = menu.get_node_or_null("SoundButton")
|
||||
if sound_btn == null:
|
||||
printerr("FAIL: SoundButton not found on MainMenu")
|
||||
quit(1)
|
||||
return
|
||||
if not sound_btn.text.contains("Sound: ON"):
|
||||
printerr("FAIL: SoundButton text should be ON, got: ", sound_btn.text)
|
||||
quit(1)
|
||||
return
|
||||
# Click button
|
||||
sound_btn.emit_signal("pressed")
|
||||
await process_frame
|
||||
if not sound_btn.text.contains("Sound: OFF"):
|
||||
printerr("FAIL: SoundButton text should be OFF after click, got: ", sound_btn.text)
|
||||
quit(1)
|
||||
return
|
||||
audio_mgr.set_sound_enabled(true, false)
|
||||
menu.queue_free()
|
||||
await process_frame
|
||||
print("PASS: MainMenu SoundButton toggle verified.")
|
||||
|
||||
# 3. Verify PauseMenu sound button
|
||||
var pause_menu = PauseMenuScript.new(true, "Test World")
|
||||
root.add_child(pause_menu)
|
||||
await process_frame
|
||||
if pause_menu._sound_button == null:
|
||||
printerr("FAIL: _sound_button not found in PauseMenu")
|
||||
quit(1)
|
||||
return
|
||||
if not pause_menu._sound_button.text.contains("Sound FX: ON"):
|
||||
printerr("FAIL: PauseMenu sound button should say ON, got: ", pause_menu._sound_button.text)
|
||||
quit(1)
|
||||
return
|
||||
pause_menu._sound_button.emit_signal("pressed")
|
||||
await process_frame
|
||||
if not pause_menu._sound_button.text.contains("Sound FX: OFF"):
|
||||
printerr("FAIL: PauseMenu sound button should say OFF after toggle, got: ", pause_menu._sound_button.text)
|
||||
quit(1)
|
||||
return
|
||||
audio_mgr.set_sound_enabled(true, false)
|
||||
pause_menu.queue_free()
|
||||
await process_frame
|
||||
print("PASS: PauseMenu sound button toggle verified.")
|
||||
|
||||
# 4. Verify in-game Game HUD SoundButton
|
||||
var game_scene := load("res://scenes/game.tscn")
|
||||
if game_scene:
|
||||
var game_node = game_scene.instantiate()
|
||||
root.add_child(game_node)
|
||||
await process_frame
|
||||
var hud_sound_btn = game_node.find_child("SoundButton", true, false) as Button
|
||||
if hud_sound_btn == null:
|
||||
printerr("FAIL: SoundButton not found in game HUD")
|
||||
quit(1)
|
||||
return
|
||||
if not hud_sound_btn.text.contains("Sound [M]"):
|
||||
printerr("FAIL: HUD sound button should say Sound [M], got: ", hud_sound_btn.text)
|
||||
quit(1)
|
||||
return
|
||||
hud_sound_btn.emit_signal("pressed")
|
||||
await process_frame
|
||||
if not hud_sound_btn.text.contains("Muted [M]"):
|
||||
printerr("FAIL: HUD sound button should say Muted [M] after click, got: ", hud_sound_btn.text)
|
||||
quit(1)
|
||||
return
|
||||
audio_mgr.set_sound_enabled(true, false)
|
||||
game_node.queue_free()
|
||||
await process_frame
|
||||
print("PASS: In-game HUD sound button toggle verified.")
|
||||
|
||||
# 5. Verify world chat API send & retrieve (if backend reachable)
|
||||
var api_client = root.get_node_or_null("ApiClient")
|
||||
if api_client != null:
|
||||
var reg_res = await api_client.register("chat_u_%d" % int(Time.get_unix_time_from_system()), "testpass123", "chat_%d@test.com" % int(Time.get_unix_time_from_system()), true)
|
||||
if reg_res.ok:
|
||||
var world_res = await api_client.create_world("ChatTestWorld_%d" % int(Time.get_unix_time_from_system()), "Chat test world")
|
||||
if world_res.ok and world_res.data is Dictionary:
|
||||
var wid: int = int(world_res.data.get("id", 0))
|
||||
var send_res = await api_client.send_world_chat_message(wid, "Test message from smoke test!")
|
||||
if not send_res.ok:
|
||||
printerr("FAIL: send_world_chat_message failed: ", send_res.detail)
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: send_world_chat_message succeeded.")
|
||||
var list_res = await api_client.get_world_chat_messages(wid)
|
||||
if not list_res.ok:
|
||||
printerr("FAIL: get_world_chat_messages failed: ", list_res.detail)
|
||||
quit(1)
|
||||
return
|
||||
var msgs: Array = []
|
||||
if list_res.data is Dictionary and list_res.data.has("messages"):
|
||||
msgs = list_res.data["messages"]
|
||||
elif list_res.data is Array:
|
||||
msgs = list_res.data
|
||||
if msgs.is_empty():
|
||||
printerr("FAIL: world chat messages empty after send")
|
||||
quit(1)
|
||||
return
|
||||
var msg_obj = msgs[0]
|
||||
var body_text: String = str(msg_obj.get("content", msg_obj.get("body", "")))
|
||||
if not body_text.contains("Test message from smoke test!"):
|
||||
printerr("FAIL: message text mismatch: ", body_text)
|
||||
quit(1)
|
||||
return
|
||||
print("PASS: World chat message sent and verified in history.")
|
||||
|
||||
print("SMOKE RESULT: PASS")
|
||||
quit(0)
|
||||
@@ -150,7 +150,7 @@ static func get_dm_messages(api: Node, conversation_id: int, before_id: int = 0,
|
||||
return await api.request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
||||
|
||||
static func send_dm_message(api: Node, conversation_id: int, content: String) -> Dictionary:
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/dm/conversations/%d/messages" % conversation_id, JSON.stringify({"content": content}), "application/json", true)
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/dm/conversations/%d/messages" % conversation_id, JSON.stringify({"body": content, "content": content}), "application/json", true)
|
||||
|
||||
static func get_world_chat_messages(api: Node, world_id: int, before_id: int = 0, limit: int = 50) -> Dictionary:
|
||||
var path := "/api/chat/world/%d/messages?limit=%d" % [world_id, limit]
|
||||
@@ -159,7 +159,7 @@ static func get_world_chat_messages(api: Node, world_id: int, before_id: int = 0
|
||||
return await api.request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
||||
|
||||
static func send_world_chat_message(api: Node, world_id: int, content: String) -> Dictionary:
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/world/%d/messages" % world_id, JSON.stringify({"content": content}), "application/json", true)
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/world/%d/messages" % world_id, JSON.stringify({"body": content, "content": content}), "application/json", true)
|
||||
|
||||
static func get_room_chat_messages(api: Node, room_id: String, before_id: int = 0, limit: int = 50) -> Dictionary:
|
||||
var path := "/api/chat/room/%s/messages?limit=%d" % [room_id.uri_encode(), limit]
|
||||
@@ -168,7 +168,7 @@ static func get_room_chat_messages(api: Node, room_id: String, before_id: int =
|
||||
return await api.request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
||||
|
||||
static func send_room_chat_message(api: Node, room_id: String, content: String) -> Dictionary:
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/room/%s/messages" % room_id.uri_encode(), JSON.stringify({"content": content}), "application/json", true)
|
||||
return await api.request(HTTPClient.METHOD_POST, "/api/chat/room/%s/messages" % room_id.uri_encode(), JSON.stringify({"body": content, "content": content}), "application/json", true)
|
||||
|
||||
static func report_message(api: Node, message_id: int, conversation_type: String, reason: String, context_text: String = "") -> Dictionary:
|
||||
var payload := {
|
||||
|
||||
Reference in New Issue
Block a user