/* tws_t6_common.gsc — shared config, HUD helpers, connect/spawn/help registries. */ #include maps\mp\_utility; #include common_scripts\utility; main() { precacheShader("white"); } init() { common_config(); register_help_command(".help", "List available commands"); level thread service_game_over_via("end_game"); level thread service_game_over_via("game_ended"); level thread service_game_over_via("intermission"); level thread help_chat_listener(); level thread on_player_connect_dispatch(); } common_config() { if (isDefined(level.hud_x)) return; // Existing generic helpers retain their screen-anchor conventions. Panels // use their own right-edge origin and never push stock HUD areas around. level.hud_x = 10; level.hud_y = 30; // Retain the legacy fields for consumers of the original common config. // The panel renderers below use dedicated row/column offsets instead. level.hud_row_match = level.hud_y; level.hud_row_round = level.hud_y + 15; level.hud_row_name = level.hud_y + 32; level.hud_row_count = level.hud_y + 54; level.hud_col_count = level.hud_x + 90; level.hud_col_name = level.hud_x + 60; level.hud_col_health = level.hud_x + 120 + 12; level.hud_health_threshold = 1.0; level.hud_panel_right = 10; level.hud_panel_top = 42; level.hud_panel_width = 220; level.hud_core_height = 100; level.hud_service_y = 108; level.hud_service_height = 124; level.hud_pad = 10; level.hud_row_header = 10; level.hud_row_timers = 34; level.hud_row_stats = 59; level.hud_row_bar = 80; level.hud_row_afk = 86; level.hud_font_small = 1.1; level.hud_font_large = 1.6; level.hud_font_mid = 1.25; level.hud_font_afk = 1.0; level.hud_bar_width = 200; level.hud_bar_height = 4; level.hud_rate = 0.1; level.hud_c_panel = (0.07, 0.10, 0.08); level.hud_panel_alpha = 0.72; level.hud_c_label = (0.72, 0.77, 0.74); level.hud_c_dim = (0.65, 0.70, 0.67); level.hud_c_count = (0.93, 0.96, 0.94); level.hud_c_name = (0.65, 0.85, 0.70); level.hud_c_high = (0.65, 0.85, 0.70); level.hud_c_mid = (1, 0.8, 0); level.hud_c_low = (1, 0.2, 0.2); level.hud_c_afk = (1, 0.8, 0.2); if (!isDefined(level.tws_spawn_handlers)) level.tws_spawn_handlers = []; } ensure_config() { if (!isDefined(level.hud_x)) common_config(); } register_spawn_handler(fn) { if (!isDefined(level.tws_spawn_handlers)) level.tws_spawn_handlers = []; level.tws_spawn_handlers[level.tws_spawn_handlers.size] = fn; } // Connection handlers also serve newcomers still waiting in spectator mode. register_connect_handler(fn) { if (!isDefined(level.tws_connect_handlers)) level.tws_connect_handlers = []; level.tws_connect_handlers[level.tws_connect_handlers.size] = fn; } // Modules register their own commands, regardless of init order. register_help_command(command, description) { if (!isDefined(level.tws_help_commands)) level.tws_help_commands = []; entry = spawnStruct(); entry.command = command; entry.description = description; level.tws_help_commands[level.tws_help_commands.size] = entry; } help_chat_listener() { for (;;) { level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; command = toLower(message); if (command == ".help" || command == ".commands") player show_help(); } } // A single per-player controller owns service priority, pages and expiry. show_help() { if (isDefined(level.tws_service_ended)) return; self service_ensure(); state = self.tws_service; service_advance(state, getTime()); state.generation++; state.help = service_request("help", service_page_count(state.commands.size, 4), state.generation); service_render(state); } // An undefined snapshot is the explicit unavailable view for manual requests. show_record(snapshot) { if (isDefined(level.tws_service_ended)) return; self service_ensure(); state = self.tws_service; service_advance(state, getTime()); count = 0; if (isDefined(snapshot)) count = snapshot.lines.size; state.generation++; state.record = service_request("record", service_page_count(count, 3), state.generation); state.record.snapshot = snapshot; service_render(state); } service_page_count(count, rows) { return int((max(count, 1) + rows - 1) / rows); } service_request(kind, pages, generation) { request = spawnStruct(); request.kind = kind; request.generation = generation; request.page = 0; request.pages = pages; request.remaining = 12000; return request; } service_current(state) { if (isDefined(state.help)) return state.help; return state.record; } service_advance(state, now) { elapsed = max(0, now - state.updated_at); state.updated_at = now; request = service_current(state); if (!isDefined(request) || state.closed) return; // Only the visible request consumes time. Help leaves the record untouched. request.remaining -= elapsed; if (request.remaining > 0) return; request.page++; request.remaining = 12000; if (request.page >= request.pages) { if (request.kind == "help") state.help = undefined; else state.record = undefined; } } // Stable order is independent of module initialization order; absent commands // stay absent. Unknown future commands follow the built-in ones in registry order. help_rank(command) { if (command == ".help") return 0; if (command == ".afk") return 1; if (command == ".back") return 2; if (command == ".join") return 3; if (command == ".leaders") return 4; if (command == ".b") return 5; if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 6; if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 7; return 8; } help_entries() { entries = []; if (!isDefined(level.tws_help_commands)) return entries; for (rank = 0; rank <= 8; rank++) { for (i = 0; i < level.tws_help_commands.size; i++) { entry = level.tws_help_commands[i]; if (help_rank(entry.command) == rank) entries[entries.size] = entry; } } return entries; } help_command_label(command) { rank = help_rank(command); if (rank == 6) return ".d "; if (rank == 7) return ".w "; return command; } help_description(entry) { rank = help_rank(entry.command); if (rank == 0) return "Show commands"; if (rank == 1) return "Enable protection"; if (rank == 2) return "Return to play"; if (rank == 3) return "Join as newcomer"; if (rank == 4) return "View records"; if (rank == 5) return "Check balance"; if (rank == 6) return "Deposit points"; if (rank == 7) return "Withdraw points"; return entry.description; } service_ensure() { if (isDefined(self.tws_service)) return; ensure_config(); state = spawnStruct(); state.elems = []; state.commands = help_entries(); state.help = undefined; state.record = undefined; state.closed = false; state.updated_at = getTime(); state.rendered = false; state.generation = 0; state.last_generation = 0; state.last_page = -1; state.record_text = []; state.help_page = -1; state.rows = []; self.tws_service = state; y = level.hud_service_y; state.background = make_panel_shader(self, 0, y, level.hud_panel_width, level.hud_service_height, level.hud_c_panel, level.hud_panel_alpha, 0); state.elems[state.elems.size] = state.background; state.help_title = service_text(self, state, 10, 10, "left", 1.1, "COMMANDS", level.hud_c_label); state.record_title = service_text(self, state, 10, 10, "left", 1.1, "MAP RECORD", level.hud_c_label); state.help_footer = service_text(self, state, 10, 108, "left", 0.9, "Four commands per page", level.hud_c_dim); state.record_footer = service_text(self, state, 10, 108, "left", 0.9, "Briefly shown - .leaders to view again", level.hud_c_dim); state.page_number = service_text(self, state, 178, 10, "right", 1.1, "", level.hud_c_dim); state.page_slash = service_text(self, state, 184, 10, "center", 1.1, "/", level.hud_c_dim); state.page_total = service_text(self, state, 210, 10, "right", 1.1, "", level.hud_c_dim); // Two fixed text columns keep the HUD element budget independent of the // command count. Strings change only at page boundaries, never per tick. state.command_back = make_panel_shader(self, 10, y + 30, 65, 68, (0.16, 0.19, 0.17), 0.9, 5); state.elems[state.elems.size] = state.command_back; state.command_column = service_text(self, state, 14, 31, "left", 1.0, "", level.hud_c_name); state.description_column = service_text(self, state, 81, 31, "left", 1.0, "", level.hud_c_count); state.round_label = service_text(self, state, 10, 32, "left", 1.1, "ROUND", level.hud_c_label); state.round_number = service_text(self, state, 54, 27, "left", 1.9, "", level.hud_c_count); state.holders_label = service_text(self, state, 10, 52, "left", 1.0, "RECORD HOLDERS", level.hud_c_label); for (i = 0; i < 3; i++) state.rows[i] = service_text(self, state, 10, 65 + i * 13, "left", 1.1, "", level.hud_c_count); state.empty = service_text(self, state, 10, 40, "left", 1.1, "No recorded high round yet", level.hud_c_count); state.unavailable = service_text(self, state, 10, 40, "left", 1.1, "Map record unavailable", level.hud_c_count); for (i = 0; i < state.elems.size; i++) { state.elems[i].alpha = 0; // Keep temporary private services out of the archived HUD bucket. // Hidden elements still occupy slots; do not allocate one per page. state.elems[i].archived = false; } self thread service_loop(state); self thread service_disconnect(state); self thread service_end_game(state); } service_text(player, state, x, y, align, scale, text, colour) { elem = make_panel_text(player, x, level.hud_service_y + y, align, scale, colour); elem.alpha = 0; if (text != "") elem SetText(text); state.elems[state.elems.size] = elem; return elem; } service_render(state) { if (state.closed) return; request = service_current(state); page = -1; generation = 0; if (isDefined(request)) { page = request.page; generation = request.generation; } if (state.rendered && state.last_generation == generation && state.last_page == page) return; state.rendered = true; state.last_generation = generation; state.last_page = page; for (i = 0; i < state.elems.size; i++) state.elems[i].alpha = 0; if (!isDefined(request)) return; state.background.alpha = level.hud_panel_alpha; state.page_number SetValue(page + 1); state.page_total SetValue(request.pages); state.page_number.alpha = 1; state.page_slash.alpha = 1; state.page_total.alpha = 1; if (request.kind == "help") { state.help_title.alpha = 1; state.help_footer.alpha = 1; if (state.help_page != page) { command_text = ""; description_text = ""; for (i = 0; i < 4; i++) { index = page * 4 + i; if (index < state.commands.size) { if (i > 0) { command_text += "\n"; description_text += "\n"; } entry = state.commands[index]; command_text += help_command_label(entry.command); description_text += help_description(entry); } } state.command_column SetText(command_text); state.description_column SetText(description_text); state.help_page = page; } state.command_back.alpha = 0.9; state.command_column.alpha = 1; state.description_column.alpha = 1; return; } state.record_title.alpha = 1; state.record_footer.alpha = 1; snapshot = request.snapshot; if (!isDefined(snapshot)) { state.unavailable.alpha = 1; return; } if (snapshot.round == 0) { state.empty.alpha = 1; return; } state.round_label.alpha = 1; state.round_number SetValue(snapshot.round); state.round_number.alpha = 1; state.holders_label.alpha = 1; for (i = 0; i < 3; i++) { index = page * 3 + i; if (index < snapshot.lines.size) { text = snapshot.lines[index]; // Skip identical text when replaying a page or resuming after help. if (!isDefined(state.record_text[i]) || state.record_text[i] != text) { state.rows[i] SetText(text); state.record_text[i] = text; } state.rows[i].alpha = 1; } } } service_loop(state) { self endon("disconnect"); level endon("tws_service_game_over"); for (;;) { service_advance(state, getTime()); service_render(state); wait 0.1; } } service_disconnect(state) { self waittill("disconnect"); service_destroy(state); } service_end_game(state) { self endon("disconnect"); level waittill("tws_service_game_over"); service_destroy(state); } service_destroy(state) { if (state.closed) return; state.closed = true; state.help = undefined; state.record = undefined; for (i = 0; i < state.elems.size; i++) { if (isDefined(state.elems[i])) state.elems[i] Destroy(); } state.elems = []; } service_game_over_via(event) { level endon("tws_service_game_over"); level waittill(event); level.tws_service_ended = true; level notify("tws_service_game_over"); } // The only connected listener; modules use the shared registries. on_player_connect_dispatch() { for (;;) { level waittill("connected", player); player thread on_player_spawned_dispatch(); player thread on_player_connected_dispatch(); } } on_player_connected_dispatch() { self endon("disconnect"); // Leave the connection handshake before invoking optional modules. wait 0.05; if (!isDefined(level.tws_connect_handlers)) return; for (i = 0; i < level.tws_connect_handlers.size; i++) self thread [[ level.tws_connect_handlers[i] ]](); } // handlers own their own self endon("disconnect") on_player_spawned_dispatch() { self endon("disconnect"); self waittill("spawned_player"); if (!isDefined(level.tws_spawn_handlers)) return; for (i = 0; i < level.tws_spawn_handlers.size; i++) self thread [[ level.tws_spawn_handlers[i] ]](); } make_text_elem(player, x, y, align_x, scale, colour) { elem = NewClientHudElem(player); // Fullscreen bypasses the safe-area inset of left/top screen anchors. elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.alignX = align_x; elem.alignY = "top"; elem.x = x; elem.y = y; elem.foreground = 1; elem.fontScale = scale; elem.alpha = 1; elem.color = colour; // black glow reads as an outline against bright geometry elem.glowColor = (0, 0, 0); elem.glowAlpha = 0.6; return elem; } make_shader_elem(player, x, y) { elem = NewClientHudElem(player); elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.alignX = "left"; elem.alignY = "top"; elem.x = x; elem.y = y; elem.foreground = 1; elem.alpha = 1; return elem; } // Panel x/y are local to the fixed upper-right core origin. A right screen // anchor keeps the panel on-screen across aspect ratios; negative offsets grow // left from that edge. Keep each engine's existing vertical anchor convention. make_panel_text(player, x, y, align_x, scale, colour) { elem = make_text_elem(player, x - level.hud_panel_width - level.hud_panel_right, level.hud_panel_top + y, align_x, scale, colour); elem.horzAlign = "right"; elem.sort = 20; elem.archived = true; return elem; } make_panel_shader(player, x, y, width, height, colour, alpha, sort) { elem = make_shader_elem(player, x - level.hud_panel_width - level.hud_panel_right, level.hud_panel_top + y); elem.horzAlign = "right"; elem.color = colour; elem.alpha = alpha; elem.sort = sort; elem.archived = true; elem SetShader("white", width, height); return elem; } destroy_on_disconnect(elems) { self waittill("disconnect"); for (i = 0; i < elems.size; i++) { if (isDefined(elems[i])) elems[i] Destroy(); } } int_dvar_or(name, fallback) { raw = GetDvar(name); if (raw == "") return fallback; return int(raw); } format_mmss(elapsed_ms) { total_secs = int(elapsed_ms / 1000); if (total_secs < 0) total_secs = 0; mins = int(total_secs / 60); secs = total_secs - (mins * 60); sec_str = "" + secs; if (secs < 10) sec_str = "0" + secs; return "" + mins + ":" + sec_str; } // T6 state guard used by the AFK control fix. player_active(player) { if (is_true(level.intermission) || is_true(level.gameended)) return false; if (!isDefined(player) || !isAlive(player)) return false; if (!isDefined(player.sessionstate) || player.sessionstate != "playing") return false; if (player maps\mp\zombies\_zm_laststand::player_is_in_laststand()) return false; if (isDefined(player.is_zombie) && player.is_zombie) return false; return true; }