FTE/SERVER: Map Voting functionality; Map rotation modes for dedicated servers

This commit is contained in:
MotoLegacy 2024-12-26 20:35:20 -05:00
parent be22fefdee
commit c7805825c7
9 changed files with 423 additions and 17 deletions

View file

@ -20,6 +20,7 @@ defs/standard.qc
#endif
shared_defs.qc
defs/custom.qc
map_rotation.qc
utilities/sound_helper.qc
weapon_stats.qc
utilities/math.qc
@ -28,6 +29,8 @@ hash_table.qc
dummies.qc
rounds.qc
main.qc
plugins/plugin_mapvote.qc
plugins/plugin_core.qc
utilities/weapon_utilities.qc
utilities/game_restart.qc
utilities/command_parser.qc

View file

@ -98,6 +98,7 @@ string Chat_GetHexCodeForPlayer(float player_id)
case 2: return "^x07E";
case 3: return "^xCA0";
case 4: return "^x5D0";
case 255: return "^xF76";
}
return "^xFFF";
}
@ -112,7 +113,14 @@ void Chat_Register(int sender, int player_id, string message)
// Append name+color to the message
string player_hex = Chat_GetHexCodeForPlayer(player_id);
string player_name = getplayerkeyvalue(sender, "name");
// Not a server message.
if (player_id != 255) {
message = strcat(player_hex, player_name, "^xCCC: ", message);
} else {
message = strcat(player_hex, message);
}
if (g_chatlines < (CHAT_LINES - 1)) {
g_chatbuffer[g_chatlines + 1] = message;

View file

@ -513,14 +513,26 @@ void (float achievement_id, optional entity who) GiveAchievement =
#ifdef FTE
void CSQC_SendChatMessage(float player_id, string message) {
void CSQC_SendChatMessage(float player_id, string message) =
{
WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
WriteByte(MSG_MULTICAST, EVENT_CHATMESSAGE);
WriteByte(MSG_MULTICAST, num_for_edict(self) - 1);
WriteByte(MSG_MULTICAST, player_id);
WriteString(MSG_MULTICAST, message);
multicast('0 0 0', MULTICAST_ALL);
}
};
void CSQC_SendChatMessageToPlayer(float player_id, string message, entity who) =
{
WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
WriteByte(MSG_MULTICAST, EVENT_CHATMESSAGE);
WriteByte(MSG_MULTICAST, num_for_edict(self) - 1);
WriteByte(MSG_MULTICAST, player_id);
WriteString(MSG_MULTICAST, message);
msg_entity = who;
multicast('0 0 0', MULTICAST_ONE);
};
/*
=================

View file

@ -40,11 +40,6 @@ void(entity client) LastStand_Begin;
void() Barrel_Hit;
void() teddy_react;
void() EndGame_Restart =
{
localcmd("restart\n");
}
// Fade to black function, creates another think for restart
void() EndGame_FadePrompt =
{
@ -56,7 +51,7 @@ void() EndGame_FadePrompt =
#ifdef FTE
self.think = EndGame_Restart;
self.think = MapRotation_Decide;
#else

View file

@ -215,12 +215,6 @@ void() precaches =
precache_model ("models/weapons/grenade/g_grenade.mdl");
precache_extra (W_BIATCH);
// perk machine defaults
//precache_model (PERK_QUICKREVIVE_DEFAULT_MODEL);
//precache_model (PERK_JUGGERNOG_DEFAULT_MODEL);
//precache_model (PERK_SPEEDCOLA_DEFAULT_MODEL);
//precache_model (PERK_)
#ifdef FTE
// FTE only has co-op, so don't precache morphine elsewhere.
@ -371,13 +365,17 @@ void() worldspawn =
clientstat(STAT_INSTA, EV_FLOAT, insta_icon);
clientstat(STAT_X2, EV_FLOAT, x2_icon);
clientstat(STAT_SPECTATING, EV_FLOAT, is_spectator);
clientstat(STAT_PLAYERNUM, EV_FLOAT, playernum); // literally useless but will be kept in case
clientstat(STAT_PLAYERNUM, EV_FLOAT, playernum);
clientstat(STAT_PLAYERSTANCE, EV_FLOAT, stance);
clientstat(STAT_FACINGENEMY, EV_FLOAT, facingenemy);
clientstat(STAT_VIEWZOOM, EV_FLOAT, viewzoom);
clientstat(STAT_MAXHEALTH, EV_FLOAT, max_health);
clientstat(STAT_PERKS, EV_FLOAT, perks);
autocvar(sv_enablechatplugins, 0);
autocvar(sv_maprotationbasename, "map_rotation");
autocvar(sv_maprotationmode, 0);
#endif // FTE
mappath = strcat("maps/", mapname);

View file

@ -0,0 +1,140 @@
/*
server/map_rotation.qc
Map Rotation Logic for Dedicated Servers.
Copyright (C) 2021-2024 NZ:P Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef FTE
#define MAPROTATION_MODE_DONTROTATE 0
#define MAPROTATION_MODE_FIXEDROTATION 1
#define MAPROTATION_MODE_RANDOM 2
//
// MapRotation_RestartMap()
// Loads the current map once again.
//
void() MapRotation_RestartMap =
{
localcmd(sprintf("changelevel %s\n", mapname));
};
//
// MapRotation_GetRandomMap()
// Loads a random map in the server's
// map directory.
//
void() MapRotation_GetRandomMap =
{
searchhandle maps;
string map_path;
string map_name;
float map_count;
float map_index;
// Grab a random map file
maps = search_begin("maps/*.bsp", 0, 0);
map_count = search_getsize(maps);
map_index = rint(random() * map_count);
map_path = search_getfilename(maps, map_index);
map_name = substring(map_path, 5, strlen(map_path)); // maps/
map_name = substring(map_name, 0, strlen(map_name) - 4); // .bsp
search_end(maps);
localcmd(sprintf("changelevel %s\n", map_name));
};
//
// MapRotation_SelectNextMap()
// Loads the next map in the map rotation
// text file.
//
void() MapRotation_SelectNextMap =
{
float rotation_file = fopen(sprintf("%s.txt", cvar_string("sv_maprotationbasename")), FILE_READ);
if (rotation_file == -1) {
MapRotation_GetRandomMap();
}
float load_loop = true;
string map_to_load = "";
string first_map = fgets(rotation_file);
string current_map = first_map;
while(load_loop) {
// End of file, use the first map in the file.
if not (current_map) {
load_loop = false;
break;
}
// Current line is for our map, so load the next
// and then break out.
if (mapname == current_map) {
map_to_load = fgets(rotation_file);
load_loop = false;
break;
}
current_map = fgets(rotation_file);
}
// If map_to_load is blank, use the first map in the rotation list.
if (map_to_load == "") {
map_to_load = first_map;
}
fclose(rotation_file);
localcmd(sprintf("changelevel %s\n", strtrim(map_to_load)));
};
//
// MapRotation_Decide()
// Called at game's end, picks an appropriate
// map rotation mode.
//
void() MapRotation_Decide =
{
float map_rotation_mode = cvar("sv_maprotationmode");
switch(map_rotation_mode) {
case MAPROTATION_MODE_DONTROTATE:
MapRotation_RestartMap();
break;
case MAPROTATION_MODE_FIXEDROTATION:
MapRotation_SelectNextMap();
break;
case MAPROTATION_MODE_RANDOM:
MapRotation_GetRandomMap();
break;
default:
MapRotation_RestartMap();
break;
}
};
#endif // FTE

View file

@ -0,0 +1,47 @@
/*
server/plugins/plugin_core.qc
In-game chat plugin core.
Copyright (C) 2021-2024 NZ:P Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef FTE
//
// Plugin command table
// command_name : Command string entered into developer console.
// command_function : QuakeC function called when command is executed.
// Returns 0 for success, 1 for failure.
//
var struct {
string command_name;
float(string params) command_function;
float requires_arguments;
string command_description;
} plugin_commands[] = {
{"mapvote", ChatPlugin_MapVote, true, "Usage: mapvote <bsp_name>\nAttempts to initiate a Map Vote. Fails if one is already in progress.\n"},
{"mapvote_y", ChatPlugin_MapVoteYes, false, "Usage: mapvote_y\n"},
{"mapvote_n", ChatPlugin_MapVoteNo, false, "Usage: mapvote_n\n"},
};
#endif // FTE

View file

@ -0,0 +1,186 @@
/*
server/plugins/plugin_core.qc
Map Vote plugin for Dedicated Servers.
Copyright (C) 2021-2024 NZ:P Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef FTE
#define PLUGIN_MAPVOTE_VOTETIMER 60
.float plugin_mapvote_voted;
float plugin_mapvote_in_progress;
string plugin_mapvote_mapchoice;
//
// ChatPlugin_MapVoteChange()
// Changes map after successful vote.
//
void() ChatPlugin_MapVoteChange =
{
localcmd(sprintf("changelevel %s\n", plugin_mapvote_mapchoice));
};
//
// ChatPlugin_MapVotePollResults
// Called at end of voting. Changes maps if vote
// passed.
//
void() ChatPlugin_MapVotePollResults =
{
float total_yes_votes = serverkeyfloat("mapvotes_y");
float total_no_votes = serverkeyfloat("mapvotes_n");
if (total_yes_votes > total_no_votes) {
CSQC_SendChatMessage(255, sprintf("[MapVote] Vote to switch map to '%s' has PASSED.", plugin_mapvote_mapchoice));
CSQC_SendChatMessage(255, "[MapVote] Changing maps in 15 seconds.");
self.think = ChatPlugin_MapVoteChange;
self.nextthink = time + 15;
} else {
CSQC_SendChatMessage(255, sprintf("[MapVote] Vote to switch map to '%s' has FAILED.", plugin_mapvote_mapchoice));
plugin_mapvote_in_progress = false;
remove(self);
}
};
//
// ChatPlugin_MapVoteClear()
// Clears any previous Map Vote infokeys.
//
void() ChatPlugin_MapVoteClear =
{
forceinfokey(world, "mapvotes_y", "0");
forceinfokey(world, "mapvotes_n", "0");
entity players = find(world, classname, "player");
while (players != world) {
players.plugin_mapvote_voted = false;
players = find(players, classname, "player");
}
entity spectators = find(world, classname, "spectator");
while (spectators != world) {
spectators.plugin_mapvote_voted = false;
spectators = find(spectators, classname, "player");
}
};
//
// ChatPlugin_MapVoteYes()
// "mapvote_y" command, votes Yes on Map Vote.
//
float(string params) ChatPlugin_MapVoteYes =
{
// Fail if no map vote is active.
if (plugin_mapvote_in_progress == false) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot vote because no Map Vote is active.", self);
return 1;
}
// Fail if we have already voted.
if (self.plugin_mapvote_voted == true) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot vote because you have already voted.", self);
return 1;
}
forceinfokey(world, "mapvotes_y", ftos(serverkeyfloat("mapvotes_y") + 1));
self.plugin_mapvote_voted = true;
CSQC_SendChatMessage(255, sprintf("[MapVote] %s voted YES to change map to '%s'", self.netname, plugin_mapvote_mapchoice));
return 0;
};
//
// ChatPlugin_MapVoteNo()
// "mapvote_n" command, votes No on Map Vote.
//
float(string params) ChatPlugin_MapVoteNo =
{
// Fail if no map vote is active.
if (plugin_mapvote_in_progress == false) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot vote because no Map Vote is active.", self);
return 1;
}
// Fail if we have already voted.
if (self.plugin_mapvote_voted == true) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot vote because you have already voted.", self);
return 1;
}
forceinfokey(world, "mapvotes_n", ftos(serverkeyfloat("mapvotes_n") + 1));
self.plugin_mapvote_voted = true;
CSQC_SendChatMessage(255, sprintf("[MapVote] %s voted NO to change map to '%s'", self.netname, plugin_mapvote_mapchoice));
return 0;
};
//
// ChatPlugin_MapVote(params)
// "mapvote" command, initiates Map Vote.
//
float(string params) ChatPlugin_MapVote =
{
// Fail if a map vote is already in progress.
if (plugin_mapvote_in_progress) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot initiate a Map Vote while one is already in progress.", self);
return 1;
}
// Fail if the requested map is the one currently active.
if (mapname == params) {
CSQC_SendChatMessageToPlayer(255, "[MapVote] Cannot vote for a map that the server is already running.", self);
return 1;
}
plugin_mapvote_mapchoice = params;
// Fail if the requested map does not exist on disk.
float bsp = fopen(sprintf("maps/%s.bsp", plugin_mapvote_mapchoice), FILE_READ);
if (bsp == -1) {
CSQC_SendChatMessageToPlayer(255, sprintf("[MapVote] Could not find map '%s' on server.", plugin_mapvote_mapchoice), self);
return 1;
}
fclose(bsp);
// Reset any old votes.
ChatPlugin_MapVoteClear();
plugin_mapvote_in_progress = true;
CSQC_SendChatMessage(255, sprintf("[MapVote] Begining vote for map '%s'", plugin_mapvote_mapchoice));
CSQC_SendChatMessage(255, "Vote with 'mapvote_y' or 'mapvote_n' to switch maps.");
CSQC_SendChatMessage(255, sprintf("Voting will end in %d seconds.", PLUGIN_MAPVOTE_VOTETIMER));
// Spawn a temporary entity to handle polling results.
entity tempe = spawn();
tempe.classname = "plugin_mapvote_poller";
tempe.think = ChatPlugin_MapVotePollResults;
tempe.nextthink = time + PLUGIN_MAPVOTE_VOTETIMER;
return 0;
};
#endif // FTE

View file

@ -196,10 +196,27 @@ float(string params) Command_say =
{
// Grab parameters.
tokenize(params);
string value = argv(0);
string first_word = argv(0);
#ifdef FTE
if (cvar("sv_enablechatplugins")) {
// Support for custom chat commands.
for (float i = 0; i < plugin_commands.length; i++) {
// Command names match
if (first_word == plugin_commands[i].command_name) {
// Return description if no params are given
if (argv(1) == "" && plugin_commands[i].requires_arguments == true) {
CSQC_SendChatMessageToPlayer(255, plugin_commands[i].command_description, self);
} else {
plugin_commands[i].command_function(argv(1));
}
return COMMAND_SUCCESS;
}
}
}
CSQC_SendChatMessage(self.playernum, params);
#endif // FTE