Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
dr3murr
9227539895 mapvote 2025-10-23 01:29:26 -04:00
6 changed files with 1793 additions and 0 deletions

View 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("=====================================")

View 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)

View 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)

View 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)

View 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
View 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)