diff options
-rw-r--r-- | locale/en.ini | 15 | ||||
-rw-r--r-- | pixel-client/src/game.rs | 5 | ||||
-rw-r--r-- | server/bot/src/main.rs | 7 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 8 | ||||
-rw-r--r-- | server/replaytool/src/main.rs | 18 | ||||
-rw-r--r-- | server/src/commands.rs | 10 | ||||
-rw-r--r-- | server/src/entity/book.rs | 3 | ||||
-rw-r--r-- | server/src/entity/campaign.rs | 10 | ||||
-rw-r--r-- | server/src/entity/mod.rs | 4 | ||||
-rw-r--r-- | server/src/entity/tutorial.rs | 4 | ||||
-rw-r--r-- | server/src/lib.rs | 39 | ||||
-rw-r--r-- | server/src/main.rs | 13 | ||||
-rw-r--r-- | server/src/server.rs | 38 | ||||
-rw-r--r-- | server/src/state.rs | 24 | ||||
-rw-r--r-- | test-client/main.ts | 13 | ||||
-rw-r--r-- | test-client/protocol.ts | 3 | ||||
-rw-r--r-- | test-client/visual.ts | 38 |
17 files changed, 156 insertions, 96 deletions
diff --git a/locale/en.ini b/locale/en.ini index 73510a7c..83beede5 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -179,6 +179,21 @@ s.tutorial.put_away=Put away this item for later s.tutorial.put_on=Place on %s s.tutorial.take=Take %s from here s.tutorial.wait_finish=... +s.replay.cannot_join=Replays cannot be joined. +s.campaign.unlock_condition=To unlock: %n%n%s +s.state.abort_no_players=Game was aborted due to a lack of players. +s.state.game_aborted=Game was aborted by %s. +s.state.overflow_resubscribe=Lagging behind. Some clientbound packets were dropped. +s.error.conn_too_many_players=Players-per-connection limit exceeded. +s.error.packet_sender_invalid=Packet sent to a player that is not owned by this connection. +s.error.no_player=Player does not exist. +s.error.no_tile=Tile does not exist. +s.error.already_interacting=already interacting +s.error.interacting_too_far=interacting too far from player +s.error.self_interact=Interacting with yourself. This is impossible. +s.error.customer_interact=You shall not interact with customers. +s.error.packet_not_supported=Packet not supported in this session. +s.error.map_load=Map failed to load: %s unknown298=Signature of the Employer:%nMusterfoods Ltd.%nFrank Miller, Head of HR unknown312=Signature of the Employee:%n%n%n unknown33=Automatic diff --git a/pixel-client/src/game.rs b/pixel-client/src/game.rs index 780cde42..07c1fc9b 100644 --- a/pixel-client/src/game.rs +++ b/pixel-client/src/game.rs @@ -359,7 +359,7 @@ impl Game { recipe: RecipeIndex(0), }); } - PacketC::ServerMessage { text: _ } => { + PacketC::ServerMessage { .. } => { // TODO } PacketC::Score(score) => { @@ -377,9 +377,6 @@ impl Game { player.message_persist = message.map(|m| (m, timeout)); } } - PacketC::Error { message } => { - warn!("server error: {message:?}") - } _ => (), } } diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs index d4d21d35..0e6aed36 100644 --- a/server/bot/src/main.rs +++ b/server/bot/src/main.rs @@ -78,8 +78,11 @@ fn main() -> Result<()> { .map(|(_, c)| c()) .unwrap_or_else(|| panic!("unknown algo {:?}", args.algo)), }), - PacketC::Error { message } => { - warn!("server error message: {message}"); + PacketC::ServerMessage { + message, + error: true, + } => { + warn!("server error message: {message:?}"); } _ => (), } diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index b8f30ce7..fb6ad46e 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -27,7 +27,7 @@ pub use glam; pub mod movement; -pub const VERSION: (u32, u32) = (6, 0); +pub const VERSION: (u32, u32) = (7, 0); pub const BINCODE_CONFIG: Configuration<LittleEndian, Varint, Limit<4096>> = standard().with_limit(); @@ -220,7 +220,8 @@ pub enum PacketC { player: PlayerID, }, ServerMessage { - text: String, + message: Message, + error: bool, }, ServerHint { #[bincode(with_serde)] @@ -232,9 +233,6 @@ pub enum PacketC { state: bool, lobby: bool, }, - Error { - message: String, - }, Menu(Menu), MovementSync { player: PlayerID, diff --git a/server/replaytool/src/main.rs b/server/replaytool/src/main.rs index a6ad70fd..530d1a76 100644 --- a/server/replaytool/src/main.rs +++ b/server/replaytool/src/main.rs @@ -19,7 +19,7 @@ use anyhow::{anyhow, Context}; use async_compression::tokio::{bufread::ZstdDecoder, write::ZstdEncoder}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; -use hurrycurry_protocol::{PacketC, PacketS}; +use hurrycurry_protocol::{Message, PacketC, PacketS}; use log::{debug, info, warn, LevelFilter}; use serde::{Deserialize, Serialize}; use std::{ @@ -32,7 +32,7 @@ use tokio::{ net::TcpListener, time::sleep, }; -use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::tungstenite; #[derive(Parser)] enum Args { @@ -121,7 +121,7 @@ async fn main() -> anyhow::Result<()> { .await?; 'outer: while let Some(Ok(message)) = sock.next().await { match message { - Message::Text(line) => { + tungstenite::Message::Text(line) => { let packet: PacketS = match serde_json::from_str(&line) { Ok(p) => p, Err(e) => { @@ -135,7 +135,11 @@ async fn main() -> anyhow::Result<()> { PacketS::Join { .. } => { sock.send(tokio_tungstenite::tungstenite::Message::Text( serde_json::to_string(&PacketC::ServerMessage { - text: "Replays cannot be joined".to_string(), + message: Message::Translation { + id: "s.replay.cannot_join".to_owned(), + params: vec![], + }, + error: true, }) .unwrap(), )) @@ -160,7 +164,7 @@ async fn main() -> anyhow::Result<()> { x => warn!("unhandled client packet: {x:?}"), } } - Message::Close(_) => break, + tungstenite::Message::Close(_) => break, _ => (), } } @@ -182,7 +186,7 @@ pub async fn do_record(output: &Path, url: &str) -> anyhow::Result<()> { while let Some(Ok(message)) = sock.next().await { match message { - Message::Text(line) => { + tungstenite::Message::Text(line) => { let packet: PacketC = serde_json::from_str(&line).context("invalid packet")?; debug!("<- {packet:?}"); @@ -206,7 +210,7 @@ pub async fn do_record(output: &Path, url: &str) -> anyhow::Result<()> { break; } } - Message::Close(_) => break, + tungstenite::Message::Close(_) => break, _ => (), } } diff --git a/server/src/commands.rs b/server/src/commands.rs index b06d9261..8e6f32c7 100644 --- a/server/src/commands.rs +++ b/server/src/commands.rs @@ -18,6 +18,7 @@ use crate::{ entity::{bot::BotDriver, tutorial::Tutorial}, server::Server, + trm, }; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; @@ -131,14 +132,17 @@ impl Server { Command::End => { self.tx .send(PacketC::ServerMessage { - text: format!( - "Game was aborted by {}.", - self.game + message: trm!( + "s.state.game_aborted", + s = self + .game .players .get(&player) .ok_or(anyhow!("player missing"))? .name + .clone() ), + error: false, }) .ok(); self.load(self.index.generate("lobby").await?, None); diff --git a/server/src/entity/book.rs b/server/src/entity/book.rs index 9c1192da..11ec847b 100644 --- a/server/src/entity/book.rs +++ b/server/src/entity/book.rs @@ -16,6 +16,7 @@ */ use super::{Entity, EntityContext}; +use crate::TrError; use anyhow::Result; use hurrycurry_protocol::{glam::IVec2, Menu, PacketC, PlayerID}; @@ -28,7 +29,7 @@ impl Entity for Book { c: EntityContext<'_>, pos: Option<IVec2>, _player: PlayerID, - ) -> Result<bool> { + ) -> Result<bool, TrError> { if pos == Some(self.0) { c.packet_out.push_back(PacketC::Menu(Menu::Book)); return Ok(true); diff --git a/server/src/entity/campaign.rs b/server/src/entity/campaign.rs index 934f7542..5d669a4d 100644 --- a/server/src/entity/campaign.rs +++ b/server/src/entity/campaign.rs @@ -16,7 +16,7 @@ */ use super::{Entity, EntityContext}; -use crate::{scoreboard::ScoreboardStore, server::GameServerExt}; +use crate::{scoreboard::ScoreboardStore, server::GameServerExt, trm, TrError}; use anyhow::Result; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, @@ -79,10 +79,14 @@ impl Entity for Gate { c: EntityContext<'_>, pos: Option<IVec2>, _player: PlayerID, - ) -> Result<bool> { + ) -> Result<bool, TrError> { if !self.unlocked && pos == Some(self.location) { c.packet_out.push_back(PacketC::ServerMessage { - text: format!("To unlock: \n\n{}", self.condition.show(c.scoreboard)), + message: trm!( + "s.campaign.unlock_condition", + s = self.condition.show(c.scoreboard) // TODO localize + ), + error: false, }); return Ok(true); } diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index f87dbb32..532031d5 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -25,7 +25,7 @@ pub mod item_portal; pub mod player_portal; pub mod tutorial; -use crate::{data::ItemTileRegistry, scoreboard::ScoreboardStore}; +use crate::{data::ItemTileRegistry, scoreboard::ScoreboardStore, TrError}; use anyhow::{anyhow, Result}; use book::Book; use campaign::{Gate, GateCondition, Map}; @@ -68,7 +68,7 @@ pub trait Entity { _c: EntityContext<'_>, _pos: Option<IVec2>, _player: PlayerID, - ) -> Result<bool> { + ) -> Result<bool, TrError> { Ok(false) } } diff --git a/server/src/entity/tutorial.rs b/server/src/entity/tutorial.rs index fba43fd0..9b2146d2 100644 --- a/server/src/entity/tutorial.rs +++ b/server/src/entity/tutorial.rs @@ -1,4 +1,4 @@ -use crate::trm; +use crate::{trm, TrError}; use super::{Entity, EntityContext}; use anyhow::Result; @@ -70,7 +70,7 @@ impl Entity for Tutorial { _c: EntityContext<'_>, _pos: Option<IVec2>, _player: PlayerID, - ) -> Result<bool> { + ) -> Result<bool, TrError> { Ok(false) } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 3969c67c..306ebd40 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -24,7 +24,7 @@ pub mod scoreboard; pub mod server; pub mod state; -use hurrycurry_protocol::glam::Vec2; +use hurrycurry_protocol::{glam::Vec2, Message}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ConnectionID(pub i64); @@ -66,3 +66,40 @@ macro_rules! trm_param { hurrycurry_protocol::Message::Tile($x) }; } + +#[derive(Debug)] +pub struct TrError { + pub id: &'static str, + pub params: Vec<Message>, +} +impl From<TrError> for Message { + fn from(value: TrError) -> Self { + Self::Translation { + id: value.id.to_owned(), + params: value.params, + } + } +} + +#[macro_export] +macro_rules! tre { + ($id:literal $(, $typ:ident = $param:expr)*) => { + crate::TrError { + id: $id, + params: vec![$(crate::tre_param!($typ, $param)),*] + } + }; +} + +#[macro_export] +macro_rules! tre_param { + (s, $x:expr) => { + hurrycurry_protocol::Message::Text($x) + }; + (i, $x:expr) => { + hurrycurry_protocol::Message::Item($x) + }; + (t, $x:expr) => { + hurrycurry_protocol::Message::Tile($x) + }; +} diff --git a/server/src/main.rs b/server/src/main.rs index de680200..20d9908d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,7 +22,7 @@ use hurrycurry_protocol::{PacketC, PacketS, BINCODE_CONFIG, VERSION}; use hurrycurry_server::{ data::DATA_DIR, server::{GameServerExt, Server}, - ConnectionID, + trm, ConnectionID, }; use log::{debug, info, trace, warn, LevelFilter}; use std::{ @@ -168,8 +168,8 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { rx = rx.resubscribe(); warn!("Client was lagging; resubscribed: {e}"); PacketC::ServerMessage { - text: "Lagging behind. Some clientbound packets were dropped." - .to_string(), + message: trm!("s.state.overflow_resubscribe"), + error: true, } } }), @@ -226,9 +226,10 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { let packet_out = match state.write().await.packet_in_outer(id, packet).await { Ok(packets) => packets, Err(e) => { - warn!("Client error: {e}"); - vec![PacketC::Error { - message: format!("{e}"), + warn!("Client error: {e:?}"); + vec![PacketC::ServerMessage { + message: e.into(), + error: true, }] } }; diff --git a/server/src/server.rs b/server/src/server.rs index 848e2bb4..c9961412 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -20,9 +20,9 @@ use crate::{ entity::{Entities, EntityContext}, interaction::{interact, tick_slot}, scoreboard::ScoreboardStore, - ConnectionID, + tre, ConnectionID, TrError, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{Context, Result}; use hurrycurry_client_lib::{Game, Involvement, Item, Player, Tile}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, @@ -341,7 +341,11 @@ impl Server { self.entities = entities; } - pub fn packet_in(&mut self, packet: PacketS, replies: &mut Vec<PacketC>) -> Result<()> { + pub fn packet_in( + &mut self, + packet: PacketS, + replies: &mut Vec<PacketC>, + ) -> Result<(), TrError> { match packet { PacketS::Join { name, @@ -362,7 +366,7 @@ impl Server { .game .players .remove(&player) - .ok_or(anyhow!("player does not exist"))?; + .ok_or(tre!("s.error.no_player"))?; self.game.players_spatial_index.remove_entry(player); @@ -394,7 +398,7 @@ impl Server { .game .players .get_mut(&player) - .ok_or(anyhow!("player does not exist"))?; + .ok_or(tre!("s.error.no_player"))?; pd.movement.input(direction, boost); @@ -437,25 +441,25 @@ impl Server { .game .players .get_mut(&pid) - .ok_or(anyhow!("player does not exist"))?; + .ok_or(tre!("s.error.no_player"))?; let (pos, edge) = match (pos, player.interacting) { (None, None) => return Ok(()), // this is silent because of auto release (None, Some(pos)) => (pos, false), (Some(pos), None) => (pos, true), - (Some(_), Some(_)) => bail!("already interacting"), + (Some(_), Some(_)) => return Err(tre!("s.error.already_interacting")), }; let entpos = pos.as_vec2() + Vec2::splat(0.5); if edge && entpos.distance(player.movement.position) > 2. { - bail!("interacting too far from player"); + return Err(tre!("s.error.interacting_too_far")); } let tile = self .game .tiles .get_mut(&pos) - .ok_or(anyhow!("tile does not exist"))?; + .ok_or(tre!("s.error.no_tile"))?; // No going back from here on @@ -476,10 +480,10 @@ impl Server { .game .players .get_many_mut([&pid, &base_pid]) - .ok_or(anyhow!("Interacting with yourself. This is impossible."))?; + .ok_or(tre!("s.error.self_interact"))?; if this.character < 0 || other.character < 0 { - bail!("You shall not interact with customers.") + return Err(tre!("s.error.customer_interact")); } interact( @@ -501,7 +505,7 @@ impl Server { .game .players .get_mut(&pid) - .ok_or(anyhow!("The player does not exist"))?; + .ok_or(tre!("s.error.no_player"))?; interact( &self.game.data, @@ -548,11 +552,7 @@ impl Server { }) } PacketS::ReplaceHand { item, player } => { - let pdata = self - .game - .players - .get_mut(&player) - .ok_or(anyhow!("The player does not exist"))?; + let pdata = self.game.players.get_mut(&player).ok_or(tre!(""))?; pdata.item = item.map(|i| Item { kind: i, active: None, @@ -568,7 +568,7 @@ impl Server { self.game.score.points += score.points; self.score_changed = true; } - PacketS::ReplayTick { .. } => bail!("Packet not supported in this session"), + PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")), } Ok(()) } @@ -681,7 +681,7 @@ impl Server { while let Some(p) = self.packet_loopback.pop_front() { if let Err(e) = self.packet_in(p, &mut vec![]) { - warn!("Internal packet errored: {e}"); + warn!("Internal packet errored: {e:?}"); } } diff --git a/server/src/state.rs b/server/src/state.rs index 09e7031e..9086248a 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -15,8 +15,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{server::Server, ConnectionID}; -use anyhow::{bail, Result}; +use crate::{server::Server, tre, trm, ConnectionID, TrError}; +use anyhow::Result; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID}; use log::{debug, trace}; @@ -41,10 +41,10 @@ impl Server { &mut self, conn: ConnectionID, packet: PacketS, - ) -> Result<Vec<PacketC>> { + ) -> Result<Vec<PacketC>, TrError> { if let Some(p) = get_packet_player(&packet) { if !self.connections.entry(conn).or_default().contains(&p) { - bail!("Packet sent to a player that is not owned by this connection."); + return Err(tre!("s.state.packet_sender_invalid")); } } let mut replies = Vec::new(); @@ -58,7 +58,8 @@ impl Server { Ok(packets) => return Ok(packets), Err(e) => { return Ok(vec![PacketC::ServerMessage { - text: format!("{e}"), + message: Message::Text(format!("{e}")), // TODO localize + error: true, }]); } } @@ -68,7 +69,7 @@ impl Server { } PacketS::Join { .. } => { if self.connections.entry(conn).or_default().len() > 8 { - bail!("Players-per-connection limit exceeded.") + return Err(tre!("s.state.conn_too_many_players")); } } _ => (), @@ -84,10 +85,17 @@ impl Server { if self.count_chefs() == 0 && !self.game.lobby { self.tx .send(PacketC::ServerMessage { - text: "Game was aborted due to a lack of players".to_string(), + message: trm!("s.state.abort_no_players"), + error: false, }) .ok(); - self.load(self.index.generate("lobby").await?, None); + self.load( + self.index + .generate("lobby") + .await + .map_err(|m| tre!("s.error.map_load", s = format!("{m}")))?, + None, + ); } Ok(replies) } diff --git a/test-client/main.ts b/test-client/main.ts index 8eab832d..2f828576 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -235,20 +235,19 @@ function packet(p: PacketC) { score.points = p.points score.time_remaining = p.time_remaining ?? null break; - case "error": - global_message = { inner: { text: p.message }, anim_size: 0., anim_position: { x: 0, y: 0 }, timeout: { initial: 5, remaining: 5 } } - console.warn(p.message) - break; case "server_message": - global_message = { inner: { text: p.text }, anim_size: 0., anim_position: { x: 0, y: 0 }, timeout: { initial: 5, remaining: 5 } } + // TODO error -> red + global_message = { inner: p.message, anim_size: 0., anim_position: { x: 0, y: 0 }, timeout: { initial: 5, remaining: 5 } } break; case "set_ingame": console.log(`ingame ${p.state}`); is_lobby = p.lobby break; - case "movement_sync": - players.get(my_id)!.position = last_server_sent_position + case "movement_sync": { + const me = players.get(my_id) + if (me) me.position = last_server_sent_position break; + } case "server_hint": if (p.message) server_hints.set(p.position + "", { inner: p.message, anim_size: 0., anim_position: p.position ? { x: p.position[0] + 0.5, y: p.position[1] + 0.5 } : players.get(my_id)!.anim_position }) else server_hints.delete(p.position + "") diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 3bceb351..b6ef9046 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -59,13 +59,12 @@ export type PacketC = | { type: "update_map", tile: Vec2, kind: TileIndex | null, neighbors: [TileIndex | null] } // A map tile was changed | { type: "communicate", player: PlayerID, message?: Message, timeout?: MessageTimeout } // A player wants to communicate something, message is null when cleared | { type: "effect", player: PlayerID, name: string } // Player sent an effect - | { type: "server_message", text: string } // Text message from the server + | { type: "server_message", message: Message, error: boolean } // Text message from the server | { type: "server_hint", message?: Message, position?: Vec2 } // Hint message from server with optional position. Message is unset to clear previous message | { type: "score" } & Score // Supplies information for score OSD | { type: "menu" } & Menu // Open a menu on the client-side | { type: "environment", effects: string[] } | { type: "set_ingame", state: boolean, lobby: boolean } // Set to false when entering the game or switching maps - | { type: "error", message: string } // Your client did something wrong. export type Menu = { menu: "book" } diff --git a/test-client/visual.ts b/test-client/visual.ts index ece31b8b..d63bee03 100644 --- a/test-client/visual.ts +++ b/test-client/visual.ts @@ -221,7 +221,7 @@ function message_str(m: Message): string { return "[unknown message type]" } -function draw_message(m: MessageData) { +function draw_message(m: MessageData, server?: boolean) { ctx.save() ctx.translate(m.anim_position.x, m.anim_position.y) const scale = Math.min(m.anim_size, 1 - nametag_scale_anim); @@ -251,20 +251,24 @@ function draw_message(m: MessageData) { ctx.translate(0, -1) ctx.textAlign = "center" - ctx.font = "15px sans-serif" - ctx.scale(2 / camera_scale, 2 / camera_scale) + ctx.font = "15px " + (server ? "monospace" : "sans-serif") + if (!server) ctx.scale(2 / camera_scale, 2 / camera_scale) + + const lines = message_str(m.inner).split("\n") + const w = lines.reduce((a, v) => Math.max(a, ctx.measureText(v).width), 0) + 10 - const text = message_str(m.inner); - const w = ctx.measureText(text).width + 30 + if (!server) ctx.translate(0, -lines.length * 15 / 2) ctx.fillStyle = "#fffa" ctx.beginPath() - ctx.roundRect(-w / 2, -15, w, 30, 5) + ctx.roundRect(-w / 2, -5, w, lines.length * 15 + 10, 5) ctx.fill() ctx.fillStyle = "black" - ctx.textBaseline = "middle" - ctx.fillText(text, 0, 0) + ctx.textAlign = "left" + ctx.textBaseline = "top" + for (let i = 0; i < lines.length; i++) + ctx.fillText(lines[i], -w / 2 + 5, i * 15) ctx.translate(0, 1) } @@ -275,22 +279,8 @@ function draw_global_message() { if (!global_message) return ctx.save() ctx.translate(canvas.width / 2, canvas.height / 6) - if ("text" in global_message.inner) { - ctx.font = "20px monospace" - const lines = global_message.inner.text.split("\n") - const w = lines.reduce((a, v) => Math.max(a, ctx.measureText(v).width), 0) + 20 - - ctx.fillStyle = "#fffa" - ctx.beginPath() - ctx.roundRect(-w / 2, -20, w, lines.length * 25 + 20, 5) - ctx.fill() - - ctx.fillStyle = "black" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - for (let i = 0; i < lines.length; i++) - ctx.fillText(lines[i], -w / 2 + 10, i * 25) - } + ctx.scale(2, 2) + draw_message(global_message, true) ctx.restore() } |