/* tws_t6_join.gsc - .join lets a newcomer leave spectator mode before round end. */ #include maps\mp\_utility; #include common_scripts\utility; // Plutonium probes both entry points; runtime setup belongs in init. main() { println("[tws_t6_join] main reached"); } init() { println("[tws_t6_join] init reached"); scripts\zm\tws_t6_common::ensure_config(); scripts\zm\tws_t6_common::register_help_command(".join", "Join mid-round (newcomers only)"); scripts\zm\tws_t6_common::register_spawn_handler(::join_spawn); level thread join_chat_listener(); } // T6 first emits spawned_player during setup, then moves late connectors // into spectator mode after 0.05 seconds. Observe the settled state instead. // The common spawn dispatcher runs only once, so keep listening ourselves. join_spawn() { self endon("disconnect"); level endon("end_game"); level endon("game_ended"); level endon("intermission"); self thread join_track_death(); for (;;) { self.tws_join_spawn_pending = true; wait 0.1; if (isDefined(self.sessionstate) && self.sessionstate == "playing") self.tws_join_has_played = true; self.tws_join_spawn_pending = false; if (is_true(self.tws_join_has_played)) return; self waittill("spawned_player"); } } // A real death during the settling window still consumes newcomer eligibility. // Never clear this flag on death, a round change, or a subsequent spawn. join_track_death() { self endon("disconnect"); level endon("end_game"); level endon("game_ended"); level endon("intermission"); self waittill("killed_player"); self.tws_join_has_played = true; } join_chat_listener() { level endon("end_game"); level endon("game_ended"); level endon("intermission"); for (;;) { level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; if (toLower(message) == ".join") player thread join_request(); } } join_request() { self endon("disconnect"); level endon("end_game"); level endon("game_ended"); level endon("intermission"); // Setup spawns do not consume eligibility; actual play and .join do. if (is_true(self.tws_join_has_played) || is_true(self.tws_join_used)) { self iPrintLnBold("^3.join is only for newcomers who have not played yet"); return; } // Fail closed while the stock connection setup is still in progress. // T6 stock does not populate self.initialized the way T5 SP zombiemode // does; use sessionstate as the readiness signal. if (is_true(self.tws_join_spawn_pending) || !isDefined(self.sessionstate) || self.sessionstate != "spectator") { self iPrintLnBold("^3.join is only available while waiting to enter the game"); return; } if (is_true(level.intermission) || is_true(level.gameended) || !isDefined(level.round_number) || level.round_number < 1 || !isDefined(level.spawnplayer) || !join_has_active_player()) { self iPrintLnBold("^3The game is not ready for .join"); return; } // Late connections can miss the stock seeding of self.spectator_respawn // (stock asserts it in spectator_respawn); pick a random map spawn point // as a fallback so the stock callback can proceed. if (!isDefined(self.spectator_respawn)) { points = getstructarray("initial_spawn_points", "targetname"); if (!isDefined(points) || points.size == 0) { self iPrintLnBold("^3No spawn point available; wait for the next round"); return; } self.spectator_respawn = points[randomInt(points.size)]; } // Match stock spectators_respawn setup. Preserve any map-specific // spawn callback if another mod already installed one. if (!isDefined(level.custom_spawnplayer)) level.custom_spawnplayer = maps\mp\zombies\_zm::spectator_respawn; // Consume BEFORE entering stock code, which may yield. Repeated chat // commands cannot start overlapping spawns or grant another life. self.tws_join_used = true; self [[level.spawnplayer]](); } join_has_active_player() { players = getPlayers(); for (i = 0; i < players.size; i++) { player = players[i]; if (isDefined(player.sessionstate) && player.sessionstate == "playing" && isAlive(player) && !(player maps\mp\zombies\_zm_laststand::player_is_in_laststand())) return true; } return false; }