mirror of
https://github.com/nzp-team/quakec.git
synced 2025-01-31 13:40:52 +00:00
FTE/SERVER: Map Voting functionality; Map rotation modes for dedicated servers
This commit is contained in:
parent
be22fefdee
commit
c7805825c7
9 changed files with 423 additions and 17 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
/*
|
||||
=================
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
140
source/server/map_rotation.qc
Normal file
140
source/server/map_rotation.qc
Normal 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
|
47
source/server/plugins/plugin_core.qc
Normal file
47
source/server/plugins/plugin_core.qc
Normal 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
|
186
source/server/plugins/plugin_mapvote.qc
Normal file
186
source/server/plugins/plugin_mapvote.qc
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue