use super::*;
use crate::test_support::entities::MockEntity;
use crate::test_support::server::mock_server;
use crate::test_support::server_tools::MockTools;
use crate::test_support::tf2::game_rules::round_rules_proxy;
use crate::test_support::tf2::game_rules::{FLAGS, KOTH_TIMERS, ROUND_STATE, World};
use crate::tf2::game_mode::Holiday;
use sdk_raw::entities::NUM_SERIAL_NUM_SHIFT_BITS;
use sdk_raw::test_support::{mock_vtable, unexpected_call};
use sdk_raw::tf2::game_mode::HOLIDAY_HALLOWEEN;
use sdk_raw::tf2::game_rules::{GR_STATE_RND_RUNNING, GR_STATE_TEAM_WIN};
use sdk_raw::tf2::objectives::{
WINREASON_ALL_POINTS_CAPTURED, WINREASON_DEFEND_UNTIL_TIME_LIMIT, WINREASON_TIMELIMIT,
};
use sdk_raw::tf2::scoreboard::{TF_TEAM_BLUE, TF_TEAM_RED};
use std::cell::RefCell;
use std::ptr::null_mut;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Call {
Win(*mut sys::CTFGameRules, c_int, c_int, bool, bool, bool, bool),
Stalemate(*mut sys::CTFGameRules, c_int, bool, bool),
}
thread_local! {
static CALLS: RefCell<Vec<Call>> = const { RefCell::new(Vec::new()) };
}
unsafe extern "C" fn is_holiday_active(_: *const sys::CTFGameRules, holiday: c_int) -> bool {
holiday == HOLIDAY_HALLOWEEN
}
unsafe extern "C" fn set_stalemate(
this: *mut sys::CTFGameRules,
reason: c_int,
force_map_reset: bool,
switch_teams: bool,
) {
CALLS.with_borrow_mut(|calls| {
calls.push(Call::Stalemate(this, reason, force_map_reset, switch_teams));
});
}
unsafe extern "C" fn set_winning_team(
this: *mut sys::CTFGameRules,
team: c_int,
reason: c_int,
force_map_reset: bool,
switch_teams: bool,
dont_add_score: bool,
final_round: bool,
) {
CALLS.with_borrow_mut(|calls| {
calls.push(Call::Win(
this,
team,
reason,
force_map_reset,
switch_teams,
dont_add_score,
final_round,
));
});
}
fn rules_with_vtable(
timers: &mut [(MockEntity, EntityHandle)],
) -> (World, MockTools, *mut sys::CTFGameRules) {
let tools = MockTools::new(null_mut());
for (timer, handle) in timers {
tools.list(timer.as_ptr(), *handle);
}
let world = World::new(Some(round_rules_proxy));
let vtable = unsafe {
mock_vtable::<sys::CTFGameRules__bindgen_vtable>(unexpected_call as *const (), |vtable| {
(&raw mut (*vtable).CTFGameRules_IsHolidayActive).write(is_holiday_active);
(&raw mut (*vtable).CTFGameRules_SetStalemate).write(set_stalemate);
(&raw mut (*vtable).CTFGameRules_SetWinningTeam).write(set_winning_team);
})
};
world.set_vtable(Box::leak(vtable));
world.put_int(true, ROUND_STATE, GR_STATE_RND_RUNNING);
CALLS.take();
let rules = world.rules.cast();
(world, tools, rules)
}
#[test]
fn rounds_end_through_the_game_rules_vtable() {
let (world, tools, object) = rules_with_vtable(&mut []);
let scope = ();
let rules = GameRules::get(mock_server(&scope)).unwrap();
let tools = tools.tools();
rules
.set_winning_team(
tools,
Some(ScoringTeam::Blue),
WinReason::AllPointsCaptured,
WinOptions::default(),
)
.unwrap();
rules
.set_winning_team(
tools,
None,
WinReason::TimeLimit,
WinOptions {
reset_map: false,
switch_teams: true,
add_score: false,
final_round: true,
},
)
.unwrap();
rules
.set_stalemate(tools, StalemateReason::Timer, StalemateOptions::default())
.unwrap();
rules
.set_stalemate(
tools,
StalemateReason::JoinedMidRound,
StalemateOptions {
reset_map: false,
switch_teams: true,
},
)
.unwrap();
assert_eq!(
CALLS.take(),
[
Call::Win(
object,
TF_TEAM_BLUE,
WINREASON_ALL_POINTS_CAPTURED,
true,
false,
false,
false
),
Call::Win(object, 0, WINREASON_TIMELIMIT, false, true, true, true),
Call::Stalemate(object, raw::STALEMATE_TIMER, true, false),
Call::Stalemate(object, raw::STALEMATE_JOIN_MID, false, true),
]
);
world.put_int(true, ROUND_STATE, GR_STATE_TEAM_WIN);
assert_eq!(
rules.set_winning_team(
tools,
Some(ScoringTeam::Red),
WinReason::OpponentsDead,
WinOptions::default(),
),
Err(RoundEndError::AlreadyWon)
);
assert_eq!(
rules.set_stalemate(tools, StalemateReason::Timer, StalemateOptions::default()),
Err(RoundEndError::AlreadyWon)
);
assert_eq!(CALLS.take(), []);
}
#[test]
fn king_of_the_hill_rounds_end_only_with_both_timers() {
let red = EntityHandle::from_raw(1 << NUM_SERIAL_NUM_SHIFT_BITS | 0x40);
let blue = EntityHandle::from_raw(3 << NUM_SERIAL_NUM_SHIFT_BITS | 0x41);
let mut timers = [
(MockEntity::new(red.to_raw()), red),
(MockEntity::new(blue.to_raw()), blue),
];
let (world, tools, object) = rules_with_vtable(&mut timers);
let scope = ();
let rules = GameRules::get(mock_server(&scope)).unwrap();
let tools = tools.tools();
let win = |rules: GameRules<'_>| {
rules.set_winning_team(
tools,
Some(ScoringTeam::Red),
WinReason::DefendedUntilTimeLimit,
WinOptions::default(),
)
};
world.put_byte(false, FLAGS, 1);
world.put_int(false, KOTH_TIMERS, EntityHandle::INVALID.to_raw() as c_int);
world.put_int(
false,
KOTH_TIMERS + 4,
EntityHandle::INVALID.to_raw() as c_int,
);
assert_eq!(rules.koth_timer(ScoringTeam::Red), Ok(None));
assert_eq!(
win(rules),
Err(RoundEndError::NoKothTimer(ScoringTeam::Red))
);
world.put_int(false, KOTH_TIMERS, red.to_raw() as c_int);
world.put_int(
false,
KOTH_TIMERS + 4,
(blue.to_raw() + (1 << NUM_SERIAL_NUM_SHIFT_BITS)) as c_int,
);
assert_eq!(rules.koth_timer(ScoringTeam::Red), Ok(Some(red)));
assert_eq!(
win(rules),
Err(RoundEndError::NoKothTimer(ScoringTeam::Blue))
);
assert_eq!(
rules.set_stalemate(tools, StalemateReason::Timer, StalemateOptions::default()),
Err(RoundEndError::NoKothTimer(ScoringTeam::Blue))
);
assert_eq!(CALLS.take(), []);
world.put_int(false, KOTH_TIMERS + 4, blue.to_raw() as c_int);
assert_eq!(rules.koth_timer(ScoringTeam::Blue), Ok(Some(blue)));
assert_eq!(win(rules), Ok(()));
assert_eq!(
CALLS.take(),
[Call::Win(
object,
TF_TEAM_RED,
WINREASON_DEFEND_UNTIL_TIME_LIMIT,
true,
false,
false,
false
)]
);
world.put_byte(false, FLAGS, 0);
world.put_int(false, KOTH_TIMERS, EntityHandle::INVALID.to_raw() as c_int);
assert_eq!(win(rules), Ok(()));
assert_eq!(CALLS.take().len(), 1);
}
#[test]
fn holidays_are_asked_of_the_game_rules() {
let (_world, _tools, _) = rules_with_vtable(&mut []);
let scope = ();
let rules = GameRules::get(mock_server(&scope)).unwrap();
assert!(rules.is_holiday_active(Holiday::Halloween));
assert!(!rules.is_holiday_active(Holiday::FullMoon));
}
#[test]
fn stalemate_reasons_round_trip_through_their_raw_values() {
for reason in StalemateReason::ALL {
assert_eq!(StalemateReason::from_raw(reason.to_raw()), Some(reason));
}
assert_eq!(StalemateReason::from_raw(3), None);
assert_eq!(StalemateReason::Timer.to_raw(), raw::STALEMATE_TIMER);
}