Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9227539895 |
6 changed files with 1793 additions and 0 deletions
76
scripts/vscripts/autorun/sv_rtv_autorun.nut
Normal file
76
scripts/vscripts/autorun/sv_rtv_autorun.nut
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// ============================================================================
|
||||
// sv_rtv_autorun.nut
|
||||
// Autorun file for RTV and map voting system
|
||||
// ============================================================================
|
||||
|
||||
printt("=====================================")
|
||||
printt("[RTV/MapVote] Initializing system...")
|
||||
printt("=====================================")
|
||||
|
||||
// Define callback functions first
|
||||
function RTV_OnClientDisconnected( player )
|
||||
{
|
||||
RTV_OnPlayerDisconnected( player )
|
||||
}
|
||||
|
||||
function Nominate_OnClientDisconnected( player )
|
||||
{
|
||||
Nominate_OnPlayerDisconnected( player )
|
||||
}
|
||||
|
||||
function CheckForVoteResultAtMapEnd()
|
||||
{
|
||||
// Wait for match to end
|
||||
while ( GetGameState() != eGameState.Postmatch )
|
||||
wait 1
|
||||
|
||||
// Check if we have a voted map to go to
|
||||
if ( "nextMapVote" in level )
|
||||
{
|
||||
wait 3 // Give time for score screen
|
||||
|
||||
printt("[RTV/MapVote] Applying voted map at map end")
|
||||
MapVote_OnMapEnd()
|
||||
}
|
||||
}
|
||||
|
||||
// Include required files
|
||||
IncludeFile( "sv_menuselect_handler" )
|
||||
IncludeFile( "sv_rtv" )
|
||||
IncludeFile( "sv_mapvote" )
|
||||
IncludeFile( "sv_nominate" )
|
||||
IncludeFile( "sv_mockplayers" )
|
||||
|
||||
// Initialize systems
|
||||
InitMenuSelectHandler()
|
||||
InitRTV()
|
||||
InitMapVote()
|
||||
InitNominate()
|
||||
InitMockPlayers()
|
||||
|
||||
// Reset RTV state on map start (autorun file runs on map start)
|
||||
RTV_OnMapStart()
|
||||
Nominate_OnMapStart()
|
||||
MockPlayers_OnMapStart()
|
||||
|
||||
// Start monitoring for vote result at map end
|
||||
thread CheckForVoteResultAtMapEnd()
|
||||
|
||||
// Register callbacks
|
||||
AddCallback_OnClientDisconnected( RTV_OnPlayerDisconnected )
|
||||
AddCallback_OnClientDisconnected( Nominate_OnPlayerDisconnected )
|
||||
|
||||
printt("=====================================")
|
||||
printt("[RTV/MapVote/Nominate] System ready!")
|
||||
printt("[RTV/MapVote/Nominate] Chat commands:")
|
||||
printt(" !rtv or !rockthevote - Start Rock The Vote")
|
||||
printt(" !nominate [map] - Nominate a map (fuzzy search)")
|
||||
printt(" !nominate - Show all maps in paginated menu")
|
||||
printt(" !revote - Reopen current vote menu")
|
||||
printt("[RTV/MapVote/Nominate] Mock player commands (sv_cheats 1):")
|
||||
printt(" mock_addplayer <name> - Add mock player")
|
||||
printt(" mock_rtv <name> - Mock player votes RTV")
|
||||
printt(" mock_vote <name> <1-9> - Mock player votes in map vote")
|
||||
printt(" mock_listplayers - List all mock players")
|
||||
printt(" mock_clearplayers - Remove all mock players")
|
||||
printt("=====================================")
|
||||
623
scripts/vscripts/sv_mapvote.nut
Normal file
623
scripts/vscripts/sv_mapvote.nut
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
// ============================================================================
|
||||
// sv_mapvote.nut
|
||||
// Map voting system with gamemode voting for r1delta
|
||||
// ============================================================================
|
||||
|
||||
// Vote Configuration
|
||||
const VOTE_DURATION = 20 // Seconds
|
||||
const VOTE_MAP_OPTIONS = 6 // Number of map options to show
|
||||
const VOTE_GAMEMODE_OPTIONS = 4 // Number of gamemode options to show
|
||||
const VOTE_WARNING_TIME = 5 // Seconds before vote to warn players
|
||||
|
||||
// Vote State
|
||||
::mapVoteState <- {
|
||||
active = false,
|
||||
isRTV = false,
|
||||
selectedGamemode = "",
|
||||
votes = {},
|
||||
options = [],
|
||||
endTime = 0,
|
||||
warningTime = 0
|
||||
}
|
||||
|
||||
// Map display names
|
||||
::MAP_NAMES <- {
|
||||
mp_airbase = "Airbase",
|
||||
mp_angel_city = "Angel City",
|
||||
mp_boneyard = "Boneyard",
|
||||
mp_colony = "Colony",
|
||||
mp_corporate = "Corporate",
|
||||
mp_fracture = "Fracture",
|
||||
mp_lagoon = "Lagoon",
|
||||
mp_nexus = "Nexus",
|
||||
mp_outpost_207 = "Outpost 207",
|
||||
mp_overlook = "Overlook",
|
||||
mp_relic = "Relic",
|
||||
mp_rise = "Rise",
|
||||
mp_smugglers_cove = "Smuggler's Cove",
|
||||
mp_training_ground = "Training Ground",
|
||||
mp_wargames = "War Games",
|
||||
mp_runoff = "Runoff",
|
||||
mp_swampland = "Swampland",
|
||||
mp_haven = "Haven",
|
||||
mp_switchback = "Export",
|
||||
mp_backwater = "Backwater",
|
||||
mp_sandtrap = "Sand Trap",
|
||||
mp_harmony_mines = "Dig Site",
|
||||
mp_zone_18 = "Zone 18",
|
||||
mp_mia = "M.I.A",
|
||||
mp_nest2 = "Nest 2",
|
||||
mp_box = "Box",
|
||||
mp_npe = "Training",
|
||||
mp_o2 = "Demeter",
|
||||
mp_lobby = "Lobby"
|
||||
}
|
||||
|
||||
// Gamemode display names
|
||||
::GAMEMODE_NAMES <- {
|
||||
tdm = "Team Deathmatch",
|
||||
aitdm = "Attrition",
|
||||
cp = "Hardpoint",
|
||||
ctf = "Capture the Flag",
|
||||
lts = "Last Titan Standing",
|
||||
mfd = "Marked for Death",
|
||||
speedball = "Speedball",
|
||||
ttdm = "Titan Deathmatch"
|
||||
}
|
||||
|
||||
function InitMapVote()
|
||||
{
|
||||
// Register chat command handler for revote
|
||||
AddCallback_OnClientChatMsg( MapVote_OnChatMessage )
|
||||
printt("[MapVote] System initialized")
|
||||
}
|
||||
|
||||
function MapVote_OnChatMessage( playerIndex, message, isTeamChat )
|
||||
{
|
||||
// Check if message starts with ! or t!
|
||||
if ( message.len() < 2 )
|
||||
return message
|
||||
|
||||
local messageStart = 0
|
||||
if ( message.len() > 2 && format("%c", message[0]) == "t" && format("%c", message[1]) == "!" )
|
||||
messageStart = 2
|
||||
else if ( format("%c", message[0]) == "!" )
|
||||
messageStart = 1
|
||||
else
|
||||
return message
|
||||
|
||||
// Extract command from message
|
||||
local command = message.slice( messageStart, message.len() ).tolower()
|
||||
|
||||
// Check if it's a revote command
|
||||
if ( command != "revote" )
|
||||
return message
|
||||
|
||||
// Get player entity
|
||||
local player = GetEntByIndex( playerIndex )
|
||||
if ( !IsValid( player ) )
|
||||
return message
|
||||
|
||||
// Execute revote command
|
||||
RevoteMap( player )
|
||||
|
||||
// Block the chat message
|
||||
return ""
|
||||
}
|
||||
|
||||
// Revote function - brings back the current vote menu if one is active
|
||||
function RevoteMap( player )
|
||||
{
|
||||
if ( !mapVoteState.active )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, "No vote is currently in progress", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Resend the current vote menu to the player
|
||||
local menuText = "Vote for Map"
|
||||
if ( mapVoteState.selectedGamemode != "" )
|
||||
menuText += " (" + GetGamemodeDisplayName( mapVoteState.selectedGamemode ) + ")"
|
||||
menuText += ":\\n\\n"
|
||||
|
||||
local keysMask = 0
|
||||
|
||||
for ( local i = 0; i < mapVoteState.options.len(); i++ )
|
||||
{
|
||||
local mapName = mapVoteState.options[i]
|
||||
local displayName = GetMapDisplayName( mapName )
|
||||
menuText += "->" + (i + 1) + ". " + displayName + "\\n"
|
||||
keysMask = keysMask | (1 << i)
|
||||
}
|
||||
|
||||
// Update player's menu context
|
||||
SetPlayerMenuContext( player, 2, { options = mapVoteState.options, gamemode = mapVoteState.selectedGamemode } )
|
||||
|
||||
// Send menu with remaining time
|
||||
local timeRemaining = mapVoteState.endTime - Time()
|
||||
if ( timeRemaining <= 0 )
|
||||
timeRemaining = 1
|
||||
|
||||
SendShowMenu( player, menuText, keysMask, timeRemaining )
|
||||
|
||||
Chat_ServerPrivateMessage( player, "Vote menu reopened - you can change your vote", false )
|
||||
}
|
||||
|
||||
function StartMapVote( isRTV = false )
|
||||
{
|
||||
if ( mapVoteState.active )
|
||||
{
|
||||
printt("[MapVote] Vote already in progress")
|
||||
return
|
||||
}
|
||||
|
||||
mapVoteState.isRTV = isRTV
|
||||
mapVoteState.active = true
|
||||
mapVoteState.votes = {}
|
||||
|
||||
local playlist = GetCurrentPlaylistName()
|
||||
local modes = GetPlaylistUniqueModes( playlist )
|
||||
|
||||
printt("[MapVote] Starting vote, playlist:", playlist, "modes:", modes.len())
|
||||
|
||||
// Check if we need to do gamemode vote first
|
||||
if ( modes.len() > 1 )
|
||||
{
|
||||
// Multiple gamemodes - show gamemode vote first
|
||||
ShowGamemodeVote( modes )
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single gamemode - go straight to map vote
|
||||
mapVoteState.selectedGamemode = modes[0]
|
||||
ShowMapVote( modes[0] )
|
||||
}
|
||||
}
|
||||
|
||||
function ShowGamemodeVote( gamemodes )
|
||||
{
|
||||
printt("[MapVote] Showing gamemode vote, options:", gamemodes.len())
|
||||
|
||||
// Limit to configured number of options
|
||||
local modeOptions = []
|
||||
local optionCount = min( gamemodes.len(), VOTE_GAMEMODE_OPTIONS )
|
||||
|
||||
for ( local i = 0; i < optionCount; i++ )
|
||||
{
|
||||
modeOptions.append( gamemodes[i] )
|
||||
}
|
||||
|
||||
mapVoteState.options = modeOptions
|
||||
mapVoteState.votes = {}
|
||||
mapVoteState.endTime = Time() + VOTE_DURATION
|
||||
|
||||
// Build menu text
|
||||
local menuText = "Vote for Gamemode:\n\n"
|
||||
local keysMask = 0
|
||||
|
||||
for ( local i = 0; i < modeOptions.len(); i++ )
|
||||
{
|
||||
local modeName = modeOptions[i]
|
||||
local displayName = GetGamemodeDisplayName( modeName )
|
||||
menuText += "->" + (i + 1) + ". " + displayName + "\n"
|
||||
keysMask = keysMask | (1 << i)
|
||||
}
|
||||
|
||||
// Send menu to all players
|
||||
local players = GetPlayerArray()
|
||||
foreach ( player in players )
|
||||
{
|
||||
SetPlayerMenuContext( player, 1, { options = modeOptions } )
|
||||
}
|
||||
|
||||
SendShowMenu( true, menuText, keysMask, VOTE_DURATION )
|
||||
|
||||
// Announce vote
|
||||
Chat_ServerBroadcast( "Voting for gamemode! Press 1-" + modeOptions.len() + " to vote." )
|
||||
|
||||
// Start timer to check results
|
||||
thread MonitorVoteCompletion( true ) // true = gamemode vote
|
||||
}
|
||||
|
||||
function ShowMapVote( gamemode )
|
||||
{
|
||||
printt("[MapVote] Showing map vote for gamemode:", gamemode)
|
||||
|
||||
mapVoteState.selectedGamemode = gamemode
|
||||
|
||||
// Get available maps for this gamemode
|
||||
local availableMaps = GetMapsForGamemode( gamemode )
|
||||
|
||||
// Sort by least played
|
||||
local sortedMaps = SortMapsByLeastPlayed( availableMaps, gamemode )
|
||||
|
||||
// Select top options
|
||||
local mapOptions = []
|
||||
local optionCount = min( sortedMaps.len(), VOTE_MAP_OPTIONS )
|
||||
|
||||
for ( local i = 0; i < optionCount; i++ )
|
||||
{
|
||||
mapOptions.append( sortedMaps[i] )
|
||||
}
|
||||
|
||||
mapVoteState.options = mapOptions
|
||||
mapVoteState.votes = {}
|
||||
mapVoteState.endTime = Time() + VOTE_DURATION
|
||||
|
||||
// Build menu text
|
||||
local menuText = "Vote for Map"
|
||||
if ( gamemode != "" )
|
||||
menuText += " (" + GetGamemodeDisplayName( gamemode ) + ")"
|
||||
menuText += ":\n\n"
|
||||
|
||||
local keysMask = 0
|
||||
|
||||
for ( local i = 0; i < mapOptions.len(); i++ )
|
||||
{
|
||||
local mapName = mapOptions[i]
|
||||
local displayName = GetMapDisplayName( mapName )
|
||||
menuText += "->" + (i + 1) + ". " + displayName + "\n"
|
||||
keysMask = keysMask | (1 << i)
|
||||
}
|
||||
|
||||
// Send menu to all players
|
||||
local players = GetPlayerArray()
|
||||
foreach ( player in players )
|
||||
{
|
||||
SetPlayerMenuContext( player, 2, { options = mapOptions, gamemode = gamemode } )
|
||||
}
|
||||
|
||||
SendShowMenu( true, menuText, keysMask, VOTE_DURATION )
|
||||
|
||||
// Announce vote
|
||||
Chat_ServerBroadcast( "Voting for map! Press 1-" + mapOptions.len() + " to vote." )
|
||||
|
||||
// Start timer to check results
|
||||
thread MonitorVoteCompletion( false ) // false = map vote
|
||||
}
|
||||
|
||||
function MonitorVoteCompletion( isGamemodeVote )
|
||||
{
|
||||
local endTime = mapVoteState.endTime
|
||||
|
||||
while ( Time() < endTime )
|
||||
{
|
||||
wait 1
|
||||
|
||||
// Check if all players have voted
|
||||
local allVoted = true
|
||||
foreach ( player in GetPlayerArray() )
|
||||
{
|
||||
if ( !(player.GetUserId() in mapVoteState.votes) )
|
||||
{
|
||||
allVoted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ( allVoted )
|
||||
{
|
||||
printt("[MapVote] All players voted, ending early")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Process results
|
||||
if ( isGamemodeVote )
|
||||
ProcessGamemodeVoteResults()
|
||||
else
|
||||
ProcessMapVoteResults()
|
||||
}
|
||||
|
||||
function ProcessGamemodeVoteResults()
|
||||
{
|
||||
local results = TallyVotes( mapVoteState.options )
|
||||
|
||||
if ( results.winnerIndex == -1 )
|
||||
{
|
||||
// No votes - pick random
|
||||
results.winnerIndex = RandomInt( mapVoteState.options.len() )
|
||||
printt("[MapVote] No gamemode votes, selected random")
|
||||
}
|
||||
|
||||
local selectedMode = mapVoteState.options[results.winnerIndex]
|
||||
mapVoteState.selectedGamemode = selectedMode
|
||||
|
||||
Chat_ServerBroadcast( GetGamemodeDisplayName( selectedMode ) + " won the gamemode vote!" )
|
||||
|
||||
wait 2
|
||||
|
||||
// Now show map vote
|
||||
ShowMapVote( selectedMode )
|
||||
}
|
||||
|
||||
function ProcessMapVoteResults()
|
||||
{
|
||||
local results = TallyVotes( mapVoteState.options )
|
||||
|
||||
if ( results.winnerIndex == -1 )
|
||||
{
|
||||
// No votes - pick random
|
||||
results.winnerIndex = RandomInt( mapVoteState.options.len() )
|
||||
printt("[MapVote] No map votes, selected random")
|
||||
}
|
||||
|
||||
local selectedMap = mapVoteState.options[results.winnerIndex]
|
||||
local selectedMode = mapVoteState.selectedGamemode
|
||||
|
||||
Chat_ServerBroadcast( GetMapDisplayName( selectedMap ) + " won the vote!" )
|
||||
|
||||
wait 2
|
||||
|
||||
// Change map
|
||||
ChangeToMap( selectedMap, selectedMode )
|
||||
}
|
||||
|
||||
function TallyVotes( options )
|
||||
{
|
||||
local voteCounts = []
|
||||
for ( local i = 0; i < options.len(); i++ )
|
||||
{
|
||||
voteCounts.append( 0 )
|
||||
}
|
||||
|
||||
// Count votes
|
||||
foreach ( playerId, voteIndex in mapVoteState.votes )
|
||||
{
|
||||
if ( voteIndex >= 0 && voteIndex < voteCounts.len() )
|
||||
voteCounts[voteIndex]++
|
||||
}
|
||||
|
||||
// Find winner
|
||||
local maxVotes = 0
|
||||
local winnerIndex = -1
|
||||
|
||||
for ( local i = 0; i < voteCounts.len(); i++ )
|
||||
{
|
||||
if ( voteCounts[i] > maxVotes )
|
||||
{
|
||||
maxVotes = voteCounts[i]
|
||||
winnerIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
printt("[MapVote] Vote results:", voteCounts, "winner:", winnerIndex)
|
||||
|
||||
return {
|
||||
voteCounts = voteCounts,
|
||||
winnerIndex = winnerIndex,
|
||||
maxVotes = maxVotes
|
||||
}
|
||||
}
|
||||
|
||||
function ChangeToMap( mapName, gamemode )
|
||||
{
|
||||
printt("[MapVote] Changing to", mapName, gamemode)
|
||||
|
||||
Chat_ServerBroadcast( "Changing to " + GetMapDisplayName( mapName ) + "..." )
|
||||
|
||||
if ( mapVoteState.isRTV )
|
||||
{
|
||||
// RTV - change immediately
|
||||
GameRules_ChangeMap( mapName, gamemode )
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular vote - change at map end
|
||||
// Store for use at map end
|
||||
if ( !("nextMapVote" in level) )
|
||||
level.nextMapVote <- {}
|
||||
|
||||
level.nextMapVote.map <- mapName
|
||||
level.nextMapVote.mode <- gamemode
|
||||
}
|
||||
|
||||
mapVoteState.active = false
|
||||
|
||||
if ( "rtvState" in getroottable() )
|
||||
{
|
||||
rtvState.changeInProgress = true
|
||||
}
|
||||
}
|
||||
|
||||
function GetMapsForGamemode( gamemode )
|
||||
{
|
||||
local playlist = GetCurrentPlaylistName()
|
||||
local combos = GetPlaylistCombos( playlist )
|
||||
local maps = []
|
||||
|
||||
foreach ( combo in combos )
|
||||
{
|
||||
if ( combo.modeName == gamemode && !ArrayContains( maps, combo.mapName ) )
|
||||
{
|
||||
// Exclude current map
|
||||
if ( combo.mapName != GetMapName() )
|
||||
maps.append( combo.mapName )
|
||||
}
|
||||
}
|
||||
|
||||
printt("[MapVote] Found", maps.len(), "maps for gamemode", gamemode)
|
||||
|
||||
return maps
|
||||
}
|
||||
|
||||
function SortMapsByLeastPlayed( maps, gamemode )
|
||||
{
|
||||
local players = GetPlayerArray()
|
||||
if ( players.len() == 0 )
|
||||
{
|
||||
// No players - shuffle randomly
|
||||
return ShuffleArray( maps )
|
||||
}
|
||||
|
||||
// Calculate average play counts for each map
|
||||
local mapPlayCounts = {}
|
||||
|
||||
foreach ( mapName in maps )
|
||||
{
|
||||
local totalPlays = 0
|
||||
local validPlayers = 0
|
||||
|
||||
foreach ( player in players )
|
||||
{
|
||||
if ( !IsValid( player ) || player.IsBot() )
|
||||
continue
|
||||
|
||||
local plays = GetMapPlayCount( player, mapName, gamemode )
|
||||
totalPlays += plays
|
||||
validPlayers++
|
||||
}
|
||||
|
||||
local avgPlays = validPlayers > 0 ? (totalPlays.tofloat() / validPlayers) : 0
|
||||
mapPlayCounts[mapName] <- avgPlays
|
||||
}
|
||||
|
||||
// Sort maps by average play count using bubble sort (closure capture issues in r1delta)
|
||||
for ( local i = 0; i < maps.len() - 1; i++ )
|
||||
{
|
||||
for ( local j = 0; j < maps.len() - i - 1; j++ )
|
||||
{
|
||||
local playsA = (maps[j] in mapPlayCounts) ? mapPlayCounts[maps[j]] : 0
|
||||
local playsB = (maps[j + 1] in mapPlayCounts) ? mapPlayCounts[maps[j + 1]] : 0
|
||||
|
||||
if ( playsA > playsB )
|
||||
{
|
||||
local temp = maps[j]
|
||||
maps[j] = maps[j + 1]
|
||||
maps[j + 1] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printt("[MapVote] Sorted maps by least played")
|
||||
|
||||
return maps
|
||||
}
|
||||
|
||||
function GetMapPlayCount( player, mapName, gamemode )
|
||||
{
|
||||
if ( !IsValid( player ) )
|
||||
return 0
|
||||
|
||||
// Try to get specific gamemode play count
|
||||
local key = "mapStats[" + mapName + "].gamesCompleted[" + gamemode + "]"
|
||||
local count = player.GetPersistentVar( key )
|
||||
|
||||
if ( count == null )
|
||||
count = 0
|
||||
|
||||
return count.tointeger()
|
||||
}
|
||||
|
||||
function GetMapDisplayName( mapName )
|
||||
{
|
||||
if ( mapName in MAP_NAMES )
|
||||
return MAP_NAMES[mapName]
|
||||
|
||||
return mapName
|
||||
}
|
||||
|
||||
function GetGamemodeDisplayName( gamemode )
|
||||
{
|
||||
if ( gamemode in GAMEMODE_NAMES )
|
||||
return GAMEMODE_NAMES[gamemode]
|
||||
|
||||
return gamemode
|
||||
}
|
||||
|
||||
function ShuffleArray( arr )
|
||||
{
|
||||
local shuffled = clone arr
|
||||
|
||||
for ( local i = shuffled.len() - 1; i > 0; i-- )
|
||||
{
|
||||
local j = RandomInt( i + 1 )
|
||||
local temp = shuffled[i]
|
||||
shuffled[i] = shuffled[j]
|
||||
shuffled[j] = temp
|
||||
}
|
||||
|
||||
return shuffled
|
||||
}
|
||||
|
||||
// Menu selection handlers (called from sv_menuselect_handler.nut)
|
||||
function HandleGamemodeVoteSelection( player, selection, data )
|
||||
{
|
||||
local playerId = player.GetUserId()
|
||||
|
||||
// Convert menu selection (1-based) to array index (0-based)
|
||||
local voteIndex = selection - 1
|
||||
|
||||
if ( voteIndex < 0 || voteIndex >= data.options.len() )
|
||||
{
|
||||
printt("[MapVote] Invalid gamemode selection:", selection)
|
||||
return
|
||||
}
|
||||
|
||||
// Record vote
|
||||
mapVoteState.votes[playerId] <- voteIndex
|
||||
|
||||
local modeName = data.options[voteIndex]
|
||||
Chat_ServerPrivateMessage( player, "You voted for: " + GetGamemodeDisplayName( modeName ), false )
|
||||
|
||||
printt("[MapVote]", player.GetPlayerName(), "voted for gamemode", modeName)
|
||||
}
|
||||
|
||||
function HandleMapVoteSelection( player, selection, data )
|
||||
{
|
||||
local playerId = player.GetUserId()
|
||||
|
||||
// Convert menu selection (1-based) to array index (0-based)
|
||||
local voteIndex = selection - 1
|
||||
|
||||
if ( voteIndex < 0 || voteIndex >= data.options.len() )
|
||||
{
|
||||
printt("[MapVote] Invalid map selection:", selection)
|
||||
return
|
||||
}
|
||||
|
||||
// Record vote
|
||||
mapVoteState.votes[playerId] <- voteIndex
|
||||
|
||||
local mapName = data.options[voteIndex]
|
||||
Chat_ServerPrivateMessage( player, "You voted for: " + GetMapDisplayName( mapName ), false )
|
||||
|
||||
printt("[MapVote]", player.GetPlayerName(), "voted for map", mapName)
|
||||
}
|
||||
|
||||
// Called at map end to apply voted map
|
||||
function MapVote_OnMapEnd()
|
||||
{
|
||||
if ( "nextMapVote" in level && !mapVoteState.isRTV )
|
||||
{
|
||||
local mapName = level.nextMapVote.map
|
||||
local gamemode = level.nextMapVote.mode
|
||||
|
||||
printt("[MapVote] Applying vote result at map end:", mapName, gamemode)
|
||||
GameRules_ChangeMap( mapName, gamemode )
|
||||
}
|
||||
else
|
||||
{
|
||||
// No vote result - normal behavior
|
||||
GameRules_EndMatch()
|
||||
}
|
||||
}
|
||||
|
||||
// Min function helper
|
||||
function min( a, b )
|
||||
{
|
||||
return a < b ? a : b
|
||||
}
|
||||
|
||||
// Globalize functions
|
||||
Globalize(InitMapVote)
|
||||
Globalize(MapVote_OnChatMessage)
|
||||
Globalize(StartMapVote)
|
||||
Globalize(ShowGamemodeVote)
|
||||
Globalize(ShowMapVote)
|
||||
Globalize(HandleGamemodeVoteSelection)
|
||||
Globalize(HandleMapVoteSelection)
|
||||
Globalize(MapVote_OnMapEnd)
|
||||
Globalize(RevoteMap)
|
||||
Globalize(GetMapDisplayName)
|
||||
Globalize(GetGamemodeDisplayName)
|
||||
96
scripts/vscripts/sv_menuselect_handler.nut
Normal file
96
scripts/vscripts/sv_menuselect_handler.nut
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// ============================================================================
|
||||
// sv_menuselect_handler.nut
|
||||
// Handles menuselect commands for all menu-based voting systems
|
||||
// ============================================================================
|
||||
|
||||
// Global menu context tracking
|
||||
// playerMenuContexts[playerIndex] = { type = 0, data = {} }
|
||||
// type: 0 = none, 1 = gamemode vote, 2 = map vote, 3 = nominate menu
|
||||
::playerMenuContexts <- {}
|
||||
|
||||
function InitMenuSelectHandler()
|
||||
{
|
||||
AddClientCommandCallback( "menuselect", ClientCommand_MenuSelect )
|
||||
printt("[MenuSelect] Handler initialized")
|
||||
}
|
||||
|
||||
function ClientCommand_MenuSelect( player, ... )
|
||||
{
|
||||
if ( vargc != 1 )
|
||||
{
|
||||
printt("[MenuSelect] Invalid argument count")
|
||||
return false
|
||||
}
|
||||
|
||||
local selection = vargv[0].tointeger()
|
||||
if ( selection == null )
|
||||
{
|
||||
printt("[MenuSelect] Invalid selection value")
|
||||
return false
|
||||
}
|
||||
|
||||
local playerIndex = player.GetEntIndex()
|
||||
|
||||
// Check if player has an active menu context
|
||||
if ( !(playerIndex in playerMenuContexts) )
|
||||
{
|
||||
printt("[MenuSelect] No active menu context for player", player.GetPlayerName())
|
||||
return true
|
||||
}
|
||||
|
||||
local context = playerMenuContexts[playerIndex]
|
||||
|
||||
// Route to appropriate handler based on menu type
|
||||
local shouldClearContext = true
|
||||
|
||||
switch ( context.type )
|
||||
{
|
||||
case 1: // Gamemode vote
|
||||
HandleGamemodeVoteSelection( player, selection, context.data )
|
||||
break
|
||||
case 2: // Map vote
|
||||
HandleMapVoteSelection( player, selection, context.data )
|
||||
break
|
||||
case 3: // Nominate menu
|
||||
shouldClearContext = HandleNominateMenuSelection( player, selection, context.data )
|
||||
break
|
||||
default:
|
||||
printt("[MenuSelect] Unknown menu type:", context.type)
|
||||
break
|
||||
}
|
||||
|
||||
// Clear menu context after processing (if handler says it's ok)
|
||||
if ( shouldClearContext )
|
||||
delete playerMenuContexts[playerIndex]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function SetPlayerMenuContext( player, menuType, data )
|
||||
{
|
||||
local playerIndex = player.GetEntIndex()
|
||||
|
||||
if ( !(playerIndex in playerMenuContexts) )
|
||||
{
|
||||
playerMenuContexts[playerIndex] <- {
|
||||
type = 0,
|
||||
data = {}
|
||||
}
|
||||
}
|
||||
|
||||
playerMenuContexts[playerIndex].type = menuType
|
||||
playerMenuContexts[playerIndex].data = data
|
||||
}
|
||||
|
||||
function ClearPlayerMenuContext( player )
|
||||
{
|
||||
local playerIndex = player.GetEntIndex()
|
||||
if ( playerIndex in playerMenuContexts )
|
||||
delete playerMenuContexts[playerIndex]
|
||||
}
|
||||
|
||||
// Globalize functions
|
||||
Globalize(InitMenuSelectHandler)
|
||||
Globalize(SetPlayerMenuContext)
|
||||
Globalize(ClearPlayerMenuContext)
|
||||
|
||||
328
scripts/vscripts/sv_mockplayers.nut
Normal file
328
scripts/vscripts/sv_mockplayers.nut
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
// ============================================================================
|
||||
// sv_mockplayers.nut
|
||||
// Mock player system for testing RTV/voting without multiple real players
|
||||
// Requires sv_cheats 1
|
||||
// ============================================================================
|
||||
|
||||
// Mock player tracking
|
||||
::mockPlayers <- {
|
||||
players = [],
|
||||
nextId = 1000
|
||||
}
|
||||
|
||||
function InitMockPlayers()
|
||||
{
|
||||
// Register commands
|
||||
AddClientCommandCallback( "mock_addplayer", ClientCommand_MockAddPlayer )
|
||||
AddClientCommandCallback( "mock_removeplayer", ClientCommand_MockRemovePlayer )
|
||||
AddClientCommandCallback( "mock_clearplayers", ClientCommand_MockClearPlayers )
|
||||
AddClientCommandCallback( "mock_rtv", ClientCommand_MockRTV )
|
||||
AddClientCommandCallback( "mock_vote", ClientCommand_MockVote )
|
||||
AddClientCommandCallback( "mock_listplayers", ClientCommand_MockListPlayers )
|
||||
|
||||
printt("[MockPlayers] System initialized - use sv_cheats 1 commands for testing")
|
||||
printt("[MockPlayers] Commands: mock_addplayer <name>, mock_rtv <name>, mock_vote <name> <option>")
|
||||
}
|
||||
|
||||
// Mock player structure
|
||||
class MockPlayer
|
||||
{
|
||||
name = ""
|
||||
userId = 0
|
||||
hasRTVoted = false
|
||||
mapVote = -1
|
||||
|
||||
constructor( playerName, id )
|
||||
{
|
||||
name = playerName
|
||||
userId = id
|
||||
}
|
||||
}
|
||||
|
||||
function ClientCommand_MockAddPlayer( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
local playerName = (vargc > 0) ? vargv[0] : ("MockPlayer" + mockPlayers.nextId)
|
||||
|
||||
local mockPlayer = MockPlayer( playerName, mockPlayers.nextId )
|
||||
mockPlayers.players.append( mockPlayer )
|
||||
mockPlayers.nextId++
|
||||
|
||||
printt("[MockPlayers] Added mock player:", playerName, "ID:", mockPlayer.userId)
|
||||
Chat_ServerBroadcast( playerName + " (mock) has joined the server" )
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_MockRemovePlayer( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( vargc < 1 )
|
||||
{
|
||||
printt("[MockPlayers] Usage: mock_removeplayer <name or index>")
|
||||
return true
|
||||
}
|
||||
|
||||
local target = vargv[0]
|
||||
|
||||
// Try as index first
|
||||
try
|
||||
{
|
||||
local index = target.tointeger()
|
||||
if ( index >= 0 && index < mockPlayers.players.len() )
|
||||
{
|
||||
local removed = mockPlayers.players[index]
|
||||
mockPlayers.players.remove( index )
|
||||
printt("[MockPlayers] Removed mock player:", removed.name)
|
||||
Chat_ServerBroadcast( removed.name + " (mock) has left the server" )
|
||||
return true
|
||||
}
|
||||
}
|
||||
catch ( e ) {}
|
||||
|
||||
// Try as name
|
||||
for ( local i = 0; i < mockPlayers.players.len(); i++ )
|
||||
{
|
||||
if ( mockPlayers.players[i].name == target )
|
||||
{
|
||||
local removed = mockPlayers.players[i]
|
||||
mockPlayers.players.remove( i )
|
||||
printt("[MockPlayers] Removed mock player:", removed.name)
|
||||
Chat_ServerBroadcast( removed.name + " (mock) has left the server" )
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
printt("[MockPlayers] Mock player not found:", target)
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_MockClearPlayers( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
local count = mockPlayers.players.len()
|
||||
mockPlayers.players = []
|
||||
|
||||
printt("[MockPlayers] Cleared all", count, "mock players")
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_MockRTV( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( vargc < 1 )
|
||||
{
|
||||
printt("[MockPlayers] Usage: mock_rtv <player name or index>")
|
||||
return true
|
||||
}
|
||||
|
||||
local mockPlayer = FindMockPlayer( vargv[0] )
|
||||
if ( mockPlayer == null )
|
||||
{
|
||||
printt("[MockPlayers] Mock player not found:", vargv[0])
|
||||
return true
|
||||
}
|
||||
|
||||
if ( mockPlayer.hasRTVoted )
|
||||
{
|
||||
printt("[MockPlayers]", mockPlayer.name, "has already RTVed")
|
||||
return true
|
||||
}
|
||||
|
||||
// Simulate RTV vote
|
||||
mockPlayer.hasRTVoted = true
|
||||
|
||||
if ( !("rtvState" in getroottable()) )
|
||||
{
|
||||
printt("[MockPlayers] RTV system not loaded")
|
||||
return true
|
||||
}
|
||||
|
||||
rtvState.votes[mockPlayer.userId] <- true
|
||||
|
||||
local currentVotes = GetRTVVoteCount() + 1 // +1 for mock vote
|
||||
local votesNeeded = CalculateRTVVotesNeeded()
|
||||
|
||||
printt("[MockPlayers]", mockPlayer.name, "voted to RTV (", currentVotes, "/", votesNeeded, ")")
|
||||
Chat_ServerBroadcast( mockPlayer.name + " (mock) wants to rock the vote! (" + currentVotes + "/" + votesNeeded + " votes)" )
|
||||
|
||||
// Check if enough votes
|
||||
if ( currentVotes >= votesNeeded )
|
||||
{
|
||||
StartRTV()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_MockVote( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( vargc < 2 )
|
||||
{
|
||||
printt("[MockPlayers] Usage: mock_vote <player name> <option 1-9>")
|
||||
return true
|
||||
}
|
||||
|
||||
local mockPlayer = FindMockPlayer( vargv[0] )
|
||||
if ( mockPlayer == null )
|
||||
{
|
||||
printt("[MockPlayers] Mock player not found:", vargv[0])
|
||||
return true
|
||||
}
|
||||
|
||||
local option = vargv[1].tointeger()
|
||||
if ( option == null || option < 1 || option > 9 )
|
||||
{
|
||||
printt("[MockPlayers] Invalid option, must be 1-9")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( !("mapVoteState" in getroottable()) )
|
||||
{
|
||||
printt("[MockPlayers] Map vote system not loaded")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( !mapVoteState.active )
|
||||
{
|
||||
printt("[MockPlayers] No active vote")
|
||||
return true
|
||||
}
|
||||
|
||||
// Simulate vote
|
||||
local voteIndex = option - 1
|
||||
|
||||
if ( voteIndex >= mapVoteState.options.len() )
|
||||
{
|
||||
printt("[MockPlayers] Invalid option for current vote")
|
||||
return true
|
||||
}
|
||||
|
||||
mapVoteState.votes[mockPlayer.userId] <- voteIndex
|
||||
|
||||
local optionName = mapVoteState.options[voteIndex]
|
||||
printt("[MockPlayers]", mockPlayer.name, "voted for option", option, "(", optionName, ")")
|
||||
Chat_ServerBroadcast( mockPlayer.name + " (mock) voted for: " + optionName )
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_MockListPlayers( player, ... )
|
||||
{
|
||||
if ( !GetConVarBool( "sv_cheats" ) )
|
||||
{
|
||||
printt("[MockPlayers] Requires sv_cheats 1")
|
||||
return true
|
||||
}
|
||||
|
||||
if ( mockPlayers.players.len() == 0 )
|
||||
{
|
||||
printt("[MockPlayers] No mock players")
|
||||
return true
|
||||
}
|
||||
|
||||
printt("[MockPlayers] Mock players:")
|
||||
for ( local i = 0; i < mockPlayers.players.len(); i++ )
|
||||
{
|
||||
local p = mockPlayers.players[i]
|
||||
printt(" [" + i + "] " + p.name + " (ID: " + p.userId + ", RTV: " + p.hasRTVoted + ")")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function FindMockPlayer( nameOrIndex )
|
||||
{
|
||||
// Try as index first
|
||||
try
|
||||
{
|
||||
local index = nameOrIndex.tointeger()
|
||||
if ( index >= 0 && index < mockPlayers.players.len() )
|
||||
return mockPlayers.players[index]
|
||||
}
|
||||
catch ( e ) {}
|
||||
|
||||
// Try as name
|
||||
foreach ( mockPlayer in mockPlayers.players )
|
||||
{
|
||||
if ( mockPlayer.name == nameOrIndex )
|
||||
return mockPlayer
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Override GetPlayerArray to include mock players in vote counts
|
||||
function GetPlayerArrayWithMocks()
|
||||
{
|
||||
local realPlayers = GetPlayerArray()
|
||||
return realPlayers.len() + mockPlayers.players.len()
|
||||
}
|
||||
|
||||
// Modify RTV vote calculation to include mocks
|
||||
function GetRTVVoteCount_WithMocks()
|
||||
{
|
||||
local count = GetRTVVoteCount() // Real player votes
|
||||
|
||||
// Add mock player votes
|
||||
foreach ( mockPlayer in mockPlayers.players )
|
||||
{
|
||||
if ( mockPlayer.hasRTVoted )
|
||||
count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
function CalculateRTVVotesNeeded_WithMocks()
|
||||
{
|
||||
local playerCount = GetPlayerArray().len() + mockPlayers.players.len()
|
||||
local needed = (playerCount * RTV_PERCENTAGE_NEEDED).tointeger()
|
||||
|
||||
if ( needed == 0 )
|
||||
needed = 1
|
||||
|
||||
return needed
|
||||
}
|
||||
|
||||
// Reset mock votes on map change
|
||||
function MockPlayers_OnMapStart()
|
||||
{
|
||||
foreach ( mockPlayer in mockPlayers.players )
|
||||
{
|
||||
mockPlayer.hasRTVoted = false
|
||||
mockPlayer.mapVote = -1
|
||||
}
|
||||
|
||||
printt("[MockPlayers] Reset votes for all mock players")
|
||||
}
|
||||
|
||||
// Globalize functions
|
||||
Globalize(InitMockPlayers)
|
||||
Globalize(MockPlayers_OnMapStart)
|
||||
371
scripts/vscripts/sv_nominate.nut
Normal file
371
scripts/vscripts/sv_nominate.nut
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
// ============================================================================
|
||||
// sv_nominate.nut
|
||||
// Map nomination system for r1delta
|
||||
// ============================================================================
|
||||
|
||||
// Nomination state
|
||||
::nominateState <- {
|
||||
nominations = {}, // mapName -> count
|
||||
playerNominations = {} // playerId -> mapName
|
||||
}
|
||||
|
||||
function InitNominate()
|
||||
{
|
||||
// Register chat command handler
|
||||
AddCallback_OnClientChatMsg( Nominate_OnChatMessage )
|
||||
printt("[Nominate] System initialized")
|
||||
}
|
||||
|
||||
function Nominate_OnChatMessage( playerIndex, message, isTeamChat )
|
||||
{
|
||||
// Check if message starts with ! or t!
|
||||
if ( message.len() < 2 )
|
||||
return message
|
||||
|
||||
local messageStart = 0
|
||||
if ( message.len() > 2 && format("%c", message[0]) == "t" && format("%c", message[1]) == "!" )
|
||||
messageStart = 2
|
||||
else if ( format("%c", message[0]) == "!" )
|
||||
messageStart = 1
|
||||
else
|
||||
return message
|
||||
|
||||
// Extract command and args from message
|
||||
local commandText = message.slice( messageStart, message.len() )
|
||||
local args = split( commandText, " " )
|
||||
|
||||
if ( args.len() == 0 )
|
||||
return message
|
||||
|
||||
local command = args[0].tolower()
|
||||
|
||||
// Check if it's a nominate command
|
||||
if ( command != "nominate" && command != "nom" )
|
||||
return message
|
||||
|
||||
// Get player entity
|
||||
local player = GetEntByIndex( playerIndex )
|
||||
if ( !IsValid( player ) )
|
||||
return message
|
||||
|
||||
// Execute nominate command
|
||||
args.remove(0)
|
||||
NominateMap( player, args )
|
||||
|
||||
// Block the chat message
|
||||
return ""
|
||||
}
|
||||
|
||||
function NominateMap( player, args )
|
||||
{
|
||||
// No args - show paginated menu of all maps
|
||||
if ( args.len() == 0 )
|
||||
{
|
||||
ShowNominateMenu( player, 0 )
|
||||
return
|
||||
}
|
||||
|
||||
// Combine args into search string
|
||||
local searchTerm = ""
|
||||
foreach ( arg in args )
|
||||
searchTerm += arg + " "
|
||||
searchTerm = searchTerm.slice( 0, searchTerm.len() - 1 ).tolower()
|
||||
|
||||
// Find matching maps (fuzzy search on both code name and friendly name)
|
||||
local matchingMaps = []
|
||||
foreach ( mapKey, mapName in MAP_NAMES )
|
||||
{
|
||||
// Check if search term is in the code name (e.g., "o2" matches "mp_o2")
|
||||
if ( mapKey.tolower().find( searchTerm ) != null )
|
||||
matchingMaps.append( mapKey )
|
||||
// Check if search term is in the friendly name (e.g., "demeter" matches "Demeter")
|
||||
else if ( mapName.tolower().find( searchTerm ) != null )
|
||||
matchingMaps.append( mapKey )
|
||||
}
|
||||
|
||||
if ( matchingMaps.len() == 0 )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, "No maps found matching '" + searchTerm + "'", false )
|
||||
return
|
||||
}
|
||||
|
||||
if ( matchingMaps.len() > 1 )
|
||||
{
|
||||
local matchList = ""
|
||||
local maxShow = 5
|
||||
for ( local i = 0; i < matchingMaps.len() && i < maxShow; i++ )
|
||||
{
|
||||
matchList += MAP_NAMES[matchingMaps[i]]
|
||||
if ( i < matchingMaps.len() - 1 && i < maxShow - 1 )
|
||||
matchList += ", "
|
||||
}
|
||||
Chat_ServerPrivateMessage( player, "Multiple matches (" + matchingMaps.len() + "): " + matchList, false )
|
||||
return
|
||||
}
|
||||
|
||||
local nominatedMap = matchingMaps[0]
|
||||
|
||||
// Check if map is in current rotation
|
||||
local playlist = GetCurrentPlaylistName()
|
||||
local playlistMaps = GetPlaylistUniqueMaps( playlist )
|
||||
if ( !ArrayContains( playlistMaps, nominatedMap ) )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, MAP_NAMES[nominatedMap] + " is not in the current rotation", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's the current map
|
||||
if ( nominatedMap == GetMapName() )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, "Cannot nominate the current map", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Remove previous nomination if exists
|
||||
local playerId = player.GetUserId()
|
||||
if ( playerId in nominateState.playerNominations )
|
||||
{
|
||||
local oldMap = nominateState.playerNominations[playerId]
|
||||
if ( oldMap in nominateState.nominations )
|
||||
{
|
||||
nominateState.nominations[oldMap]--
|
||||
if ( nominateState.nominations[oldMap] <= 0 )
|
||||
delete nominateState.nominations[oldMap]
|
||||
}
|
||||
}
|
||||
|
||||
// Add new nomination
|
||||
nominateState.playerNominations[playerId] <- nominatedMap
|
||||
if ( !(nominatedMap in nominateState.nominations) )
|
||||
nominateState.nominations[nominatedMap] <- 0
|
||||
nominateState.nominations[nominatedMap]++
|
||||
|
||||
Chat_ServerBroadcast( player.GetPlayerName() + " nominated " + MAP_NAMES[nominatedMap] + " (" + nominateState.nominations[nominatedMap] + " votes)" )
|
||||
}
|
||||
|
||||
function GetNominatedMaps()
|
||||
{
|
||||
// Sort nominations by vote count
|
||||
local sortedMaps = []
|
||||
|
||||
foreach ( mapName, count in nominateState.nominations )
|
||||
{
|
||||
sortedMaps.append( { map = mapName, count = count } )
|
||||
}
|
||||
|
||||
// Bubble sort by count (descending)
|
||||
for ( local i = 0; i < sortedMaps.len() - 1; i++ )
|
||||
{
|
||||
for ( local j = 0; j < sortedMaps.len() - i - 1; j++ )
|
||||
{
|
||||
if ( sortedMaps[j].count < sortedMaps[j + 1].count )
|
||||
{
|
||||
local temp = sortedMaps[j]
|
||||
sortedMaps[j] = sortedMaps[j + 1]
|
||||
sortedMaps[j + 1] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local result = []
|
||||
foreach ( entry in sortedMaps )
|
||||
result.append( entry.map )
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function Nominate_OnMapStart()
|
||||
{
|
||||
nominateState.nominations = {}
|
||||
nominateState.playerNominations = {}
|
||||
printt("[Nominate] Nominations reset for new map")
|
||||
}
|
||||
|
||||
function Nominate_OnPlayerDisconnected( player )
|
||||
{
|
||||
local playerId = player.GetUserId()
|
||||
|
||||
if ( playerId in nominateState.playerNominations )
|
||||
{
|
||||
local nominatedMap = nominateState.playerNominations[playerId]
|
||||
if ( nominatedMap in nominateState.nominations )
|
||||
{
|
||||
nominateState.nominations[nominatedMap]--
|
||||
if ( nominateState.nominations[nominatedMap] <= 0 )
|
||||
delete nominateState.nominations[nominatedMap]
|
||||
}
|
||||
delete nominateState.playerNominations[playerId]
|
||||
}
|
||||
}
|
||||
function ShowNominateMenu( player, page )
|
||||
{
|
||||
// Get only maps in current playlist rotation
|
||||
local playlist = GetCurrentPlaylistName()
|
||||
local playlistMaps = GetPlaylistUniqueMaps( playlist )
|
||||
local availableMaps = []
|
||||
|
||||
foreach ( mapKey in playlistMaps )
|
||||
{
|
||||
// Skip lobby and current map
|
||||
if ( mapKey == "mp_lobby" || mapKey == GetMapName() )
|
||||
continue
|
||||
if ( mapKey in MAP_NAMES )
|
||||
availableMaps.append( mapKey )
|
||||
}
|
||||
|
||||
// Paging model: page 0 has up to 8 maps, subsequent pages up to 7 maps
|
||||
local N = availableMaps.len()
|
||||
local firstCap = 8
|
||||
local otherCap = 7
|
||||
|
||||
local totalPages = (N <= firstCap) ? 1 : 1 + ((N - firstCap + otherCap - 1) / otherCap)
|
||||
|
||||
if ( totalPages <= 0 )
|
||||
totalPages = 1
|
||||
|
||||
// Normalize requested page
|
||||
if ( page < 0 )
|
||||
page = totalPages - 1
|
||||
else if ( page >= totalPages )
|
||||
page = 0
|
||||
|
||||
// Compute slice
|
||||
local startIdx, cap
|
||||
if ( page == 0 )
|
||||
{
|
||||
startIdx = 0
|
||||
cap = firstCap
|
||||
}
|
||||
else
|
||||
{
|
||||
startIdx = firstCap + (page - 1) * otherCap
|
||||
cap = otherCap
|
||||
}
|
||||
local endIdx = startIdx + cap
|
||||
if ( endIdx > N )
|
||||
endIdx = N
|
||||
|
||||
// Build menu
|
||||
local menuText = "Nominate Map (Page " + (page + 1) + "/" + totalPages + "):\n\n"
|
||||
local menuOptions = []
|
||||
local keysMask = 0
|
||||
|
||||
for ( local i = startIdx; i < endIdx; i++ )
|
||||
{
|
||||
local mapKey = availableMaps[i]
|
||||
local displayName = MAP_NAMES[mapKey]
|
||||
local optionNum = (i - startIdx) + 1 // 1..cap (max 8 on first, 7 otherwise)
|
||||
menuText += "->" + optionNum + ". " + displayName + "\n"
|
||||
menuOptions.append( mapKey )
|
||||
keysMask = keysMask | (1 << (optionNum - 1)) // bits 0..(cap-1)
|
||||
}
|
||||
|
||||
// Navigation: 8=Prev (only if page>0), 9=Next (only if page<last), 0=Cancel always
|
||||
if ( page > 0 )
|
||||
{
|
||||
menuText += "->8. Previous Page\n"
|
||||
keysMask = keysMask | (1 << 7) // key 8
|
||||
}
|
||||
|
||||
if ( page < totalPages - 1 )
|
||||
{
|
||||
menuText += "->9. Next Page\n"
|
||||
keysMask = keysMask | (1 << 8) // key 9
|
||||
}
|
||||
|
||||
menuText += "->0. Cancel\n"
|
||||
keysMask = keysMask | (1 << 9) // key 0
|
||||
|
||||
// Store context
|
||||
SetPlayerMenuContext( player, 3, { options = menuOptions, page = page, totalPages = totalPages } )
|
||||
|
||||
// Send menu
|
||||
SendShowMenu( player, menuText, keysMask, 60 )
|
||||
}
|
||||
|
||||
function HandleNominateMenuSelection( player, selection, data )
|
||||
{
|
||||
// 0 key = cancel
|
||||
if ( selection == 10 )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, "Nomination cancelled", false )
|
||||
return true // Clear menu context
|
||||
}
|
||||
|
||||
local totalPages = ("totalPages" in data) ? data.totalPages : 1
|
||||
|
||||
// 8 key = previous page (only valid when page > 0)
|
||||
if ( selection == 8 && data.page > 0 )
|
||||
{
|
||||
ShowNominateMenu( player, data.page - 1 )
|
||||
return false // Keep context for pagination
|
||||
}
|
||||
|
||||
// 9 key = next page (only valid when not on last page)
|
||||
if ( selection == 9 && data.page < totalPages - 1 )
|
||||
{
|
||||
ShowNominateMenu( player, data.page + 1 )
|
||||
return false // Keep context for pagination
|
||||
}
|
||||
|
||||
// Otherwise, treat as map selection
|
||||
local voteIndex = selection - 1
|
||||
if ( voteIndex < 0 || voteIndex >= data.options.len() )
|
||||
{
|
||||
printt("[Nominate] Invalid selection:", selection)
|
||||
return
|
||||
}
|
||||
|
||||
local nominatedMap = data.options[voteIndex]
|
||||
|
||||
// Check if map is in current rotation
|
||||
local playlist = GetCurrentPlaylistName()
|
||||
local playlistMaps = GetPlaylistUniqueMaps( playlist )
|
||||
if ( !ArrayContains( playlistMaps, nominatedMap ) )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, MAP_NAMES[nominatedMap] + " is not in the current rotation", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's the current map
|
||||
if ( nominatedMap == GetMapName() )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, "Cannot nominate the current map", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Remove previous nomination if exists
|
||||
local playerId = player.GetUserId()
|
||||
if ( playerId in nominateState.playerNominations )
|
||||
{
|
||||
local oldMap = nominateState.playerNominations[playerId]
|
||||
if ( oldMap in nominateState.nominations )
|
||||
{
|
||||
nominateState.nominations[oldMap]--
|
||||
if ( nominateState.nominations[oldMap] <= 0 )
|
||||
delete nominateState.nominations[oldMap]
|
||||
}
|
||||
}
|
||||
|
||||
// Add new nomination
|
||||
nominateState.playerNominations[playerId] <- nominatedMap
|
||||
if ( !(nominatedMap in nominateState.nominations) )
|
||||
nominateState.nominations[nominatedMap] <- 0
|
||||
nominateState.nominations[nominatedMap]++
|
||||
|
||||
Chat_ServerBroadcast( player.GetPlayerName() + " nominated " + MAP_NAMES[nominatedMap] + " (" + nominateState.nominations[nominatedMap] + " votes)" )
|
||||
printt("[Nominate]", player.GetPlayerName(), "nominated", nominatedMap)
|
||||
|
||||
return true // Clear menu context after actual nomination
|
||||
}
|
||||
|
||||
|
||||
// Globalize functions
|
||||
Globalize(InitNominate)
|
||||
Globalize(Nominate_OnChatMessage)
|
||||
Globalize(NominateMap)
|
||||
Globalize(ShowNominateMenu)
|
||||
Globalize(GetNominatedMaps)
|
||||
Globalize(Nominate_OnMapStart)
|
||||
Globalize(Nominate_OnPlayerDisconnected)
|
||||
Globalize(HandleNominateMenuSelection)
|
||||
299
scripts/vscripts/sv_rtv.nut
Normal file
299
scripts/vscripts/sv_rtv.nut
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
// ============================================================================
|
||||
// sv_rtv.nut
|
||||
// Rock The Vote system for r1delta
|
||||
// ============================================================================
|
||||
|
||||
// RTV Configuration
|
||||
const RTV_PERCENTAGE_NEEDED = 0.60 // 60% of players need to RTV
|
||||
const RTV_MIN_PLAYERS = 1 // Minimum players required for RTV
|
||||
const RTV_COOLDOWN = 120.0 // Cooldown in seconds after failed RTV
|
||||
const RTV_INITIAL_DELAY = 30.0 // Delay after map start before RTV is available
|
||||
|
||||
// RTV State
|
||||
::rtvState <- {
|
||||
allowed = false,
|
||||
votes = {},
|
||||
votesNeeded = 0,
|
||||
nextVoteTime = 0,
|
||||
hasVoteStarted = false,
|
||||
changeInProgress = false
|
||||
}
|
||||
|
||||
function InitRTV()
|
||||
{
|
||||
// Register RTV commands (both console and chat)
|
||||
AddClientCommandCallback( "rtv", ClientCommand_RTV )
|
||||
AddClientCommandCallback( "rockthevote", ClientCommand_RTV )
|
||||
|
||||
// Register chat command handler
|
||||
AddCallback_OnClientChatMsg( RTV_OnChatMessage )
|
||||
printt("[RTV] Registered RTV commands (console and chat)")
|
||||
|
||||
// Delay RTV availability
|
||||
thread DelayRTVAvailability()
|
||||
|
||||
printt("[RTV] System initialized")
|
||||
}
|
||||
|
||||
function DelayRTVAvailability()
|
||||
{
|
||||
wait RTV_INITIAL_DELAY
|
||||
rtvState.allowed = true
|
||||
printt("[RTV] Now available to players")
|
||||
}
|
||||
|
||||
function RTVCommand( player, args, returnfunc )
|
||||
{
|
||||
AttemptRTV( player, returnfunc )
|
||||
return true
|
||||
}
|
||||
|
||||
function ClientCommand_RTV( player, ... )
|
||||
{
|
||||
AttemptRTV( player, Chat_ServerPrivateMessage )
|
||||
return true
|
||||
}
|
||||
|
||||
function RTV_OnChatMessage( playerIndex, message, isTeamChat )
|
||||
{
|
||||
// Check if message starts with ! or t! (team chat)
|
||||
if ( message.len() < 2 )
|
||||
return message
|
||||
|
||||
local messageStart = 0
|
||||
if ( message.len() > 2 && format("%c", message[0]) == "t" && format("%c", message[1]) == "!" )
|
||||
messageStart = 2
|
||||
else if ( format("%c", message[0]) == "!" )
|
||||
messageStart = 1
|
||||
else
|
||||
return message
|
||||
|
||||
// Extract command from message
|
||||
local command = message.slice( messageStart, message.len() ).tolower()
|
||||
|
||||
// Check if it's an RTV command
|
||||
if ( command != "rtv" && command != "rockthevote" )
|
||||
return message
|
||||
|
||||
// Get player entity
|
||||
local player = GetEntByIndex( playerIndex )
|
||||
if ( !IsValid( player ) )
|
||||
return message
|
||||
|
||||
// Execute RTV command
|
||||
AttemptRTV( player, Chat_ServerPrivateMessage )
|
||||
|
||||
// Block the chat message
|
||||
return ""
|
||||
}
|
||||
|
||||
function AttemptRTV( player, returnfunc )
|
||||
{
|
||||
// Check if RTV is allowed
|
||||
if ( !rtvState.allowed )
|
||||
{
|
||||
returnfunc( player, "RTV is not available yet. Please wait.", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're in lobby
|
||||
if ( GetMapName() == "mp_lobby" )
|
||||
{
|
||||
returnfunc( player, "Cannot RTV in lobby.", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if ( Time() < rtvState.nextVoteTime )
|
||||
{
|
||||
local timeLeft = (rtvState.nextVoteTime - Time()).tointeger()
|
||||
returnfunc( player, "RTV is on cooldown for " + timeLeft + " more seconds.", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if vote already started
|
||||
if ( rtvState.hasVoteStarted )
|
||||
{
|
||||
returnfunc( player, "Map vote is already in progress!", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if map change is in progress
|
||||
if ( rtvState.changeInProgress )
|
||||
{
|
||||
returnfunc( player, "Map change is already in progress!", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check minimum players
|
||||
local playerCount = GetPlayerArray().len()
|
||||
if ( playerCount < RTV_MIN_PLAYERS )
|
||||
{
|
||||
returnfunc( player, "Not enough players online to start RTV (need at least " + RTV_MIN_PLAYERS + " players).", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Check if player already voted
|
||||
local playerId = player.GetUserId()
|
||||
if ( playerId in rtvState.votes )
|
||||
{
|
||||
local currentVotes = GetRTVVoteCount()
|
||||
local votesNeeded = CalculateRTVVotesNeeded()
|
||||
returnfunc( player, "You have already voted to RTV! (" + currentVotes + "/" + votesNeeded + " votes)", false )
|
||||
return
|
||||
}
|
||||
|
||||
// Record vote
|
||||
rtvState.votes[playerId] <- true
|
||||
local currentVotes = GetRTVVoteCount()
|
||||
local votesNeeded = CalculateRTVVotesNeeded()
|
||||
|
||||
// Announce vote
|
||||
local msg = player.GetPlayerName() + " wants to rock the vote! (" + currentVotes + "/" + votesNeeded + " votes)"
|
||||
Chat_ServerBroadcast( msg )
|
||||
|
||||
// Check if we have enough votes
|
||||
if ( currentVotes >= votesNeeded )
|
||||
{
|
||||
StartRTV()
|
||||
}
|
||||
}
|
||||
|
||||
function GetRTVVoteCount()
|
||||
{
|
||||
// Count votes from players who are still connected
|
||||
local validVotes = 0
|
||||
local currentPlayerIds = []
|
||||
|
||||
foreach ( player in GetPlayerArray() )
|
||||
{
|
||||
currentPlayerIds.append( player.GetUserId() )
|
||||
}
|
||||
|
||||
foreach ( playerId, voted in rtvState.votes )
|
||||
{
|
||||
if ( ArrayContains( currentPlayerIds, playerId ) )
|
||||
validVotes++
|
||||
}
|
||||
|
||||
return validVotes
|
||||
}
|
||||
|
||||
function CalculateRTVVotesNeeded()
|
||||
{
|
||||
local playerCount = GetPlayerArray().len()
|
||||
local needed = (playerCount * RTV_PERCENTAGE_NEEDED).tointeger()
|
||||
|
||||
if ( needed == 0 )
|
||||
needed = 1
|
||||
|
||||
return needed
|
||||
}
|
||||
|
||||
function StartRTV()
|
||||
{
|
||||
rtvState.hasVoteStarted = true
|
||||
|
||||
Chat_ServerBroadcast( "RTV vote passed! Starting map vote..." )
|
||||
printt("[RTV] Vote passed, starting map vote")
|
||||
|
||||
// Start map vote (implemented in sv_mapvote.nut)
|
||||
thread StartMapVote( true ) // true = is RTV
|
||||
}
|
||||
|
||||
function ResetRTV()
|
||||
{
|
||||
rtvState.votes = {}
|
||||
rtvState.hasVoteStarted = false
|
||||
rtvState.nextVoteTime = Time() + RTV_COOLDOWN
|
||||
printt("[RTV] Vote state reset, cooldown active")
|
||||
}
|
||||
|
||||
// Called when a player disconnects
|
||||
function RTV_OnPlayerDisconnected( player )
|
||||
{
|
||||
local playerId = player.GetUserId()
|
||||
|
||||
if ( playerId in rtvState.votes )
|
||||
delete rtvState.votes[playerId]
|
||||
|
||||
// Check if we still have enough votes
|
||||
if ( !rtvState.hasVoteStarted && rtvState.votes.len() > 0 )
|
||||
{
|
||||
local currentVotes = GetRTVVoteCount()
|
||||
local votesNeeded = CalculateRTVVotesNeeded()
|
||||
|
||||
if ( currentVotes >= votesNeeded )
|
||||
{
|
||||
StartRTV()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called on map start
|
||||
function RTV_OnMapStart()
|
||||
{
|
||||
rtvState.votes = {}
|
||||
rtvState.hasVoteStarted = false
|
||||
rtvState.changeInProgress = false
|
||||
rtvState.allowed = false
|
||||
thread DelayRTVAvailability()
|
||||
printt("[RTV] State reset for new map")
|
||||
}
|
||||
|
||||
// Helper function for broadcasting chat messages
|
||||
function Chat_ServerBroadcast( message )
|
||||
{
|
||||
if ( "LSendChatMsg" in getroottable() )
|
||||
{
|
||||
LSendChatMsg( true, 0, message, false, false )
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: send to all players individually
|
||||
foreach ( player in GetPlayerArray() )
|
||||
{
|
||||
Chat_ServerPrivateMessage( player, message, false )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Chat_ServerPrivateMessage( player, message, isTeam )
|
||||
{
|
||||
if ( "LSendChatMsg" in getroottable() )
|
||||
{
|
||||
LSendChatMsg( player, 0, message, isTeam, false )
|
||||
}
|
||||
else if ( "SendChatMsg" in getroottable() )
|
||||
{
|
||||
SendChatMsg( player, 0, message, isTeam, false )
|
||||
}
|
||||
}
|
||||
|
||||
// Admin command to force RTV
|
||||
function ForceRTV( player, args, returnfunc )
|
||||
{
|
||||
if ( rtvState.hasVoteStarted )
|
||||
{
|
||||
returnfunc( player, "Map vote is already in progress!", false )
|
||||
return true
|
||||
}
|
||||
|
||||
Chat_ServerBroadcast( player.GetPlayerName() + " forced a map vote!" )
|
||||
StartRTV()
|
||||
return true
|
||||
}
|
||||
|
||||
// Globalize functions
|
||||
Globalize(InitRTV)
|
||||
Globalize(RTV_OnChatMessage)
|
||||
Globalize(AttemptRTV)
|
||||
Globalize(GetRTVVoteCount)
|
||||
Globalize(CalculateRTVVotesNeeded)
|
||||
Globalize(StartRTV)
|
||||
Globalize(ResetRTV)
|
||||
Globalize(RTV_OnPlayerDisconnected)
|
||||
Globalize(RTV_OnMapStart)
|
||||
Globalize(Chat_ServerBroadcast)
|
||||
Globalize(Chat_ServerPrivateMessage)
|
||||
Globalize(ForceRTV)
|
||||
Loading…
Add table
Add a link
Reference in a new issue