first commit

This commit is contained in:
chacha 2023-04-20 14:46:12 +02:00
parent 5acd55ace2
commit edc5651395
5 changed files with 610 additions and 3 deletions

View File

@ -1,5 +1,60 @@
# UT99-Mod-ChaChaRESTStats
A tiny mod to allow reading server stats with REST requests.
/!\ Require UT99 v451+, not compatible with v436, prefered 469+.
A tiny mod to allow reading server stats with REST requests.
!!! Require UT99 v451+, not compatible with v436, prefered 469+.
# Installation
## On Linux (crudini required)
./Run.sh <Your UT99 Installation Path> (<custom UnrealTounrnament.ini>)
## Manual installation
- Copy __ChaChaRESTStats.u__ file to your UT99 __System__ dir
- Edit UnrealTournament.ini and update / adapt [UWeb.WebServer] section:
[UWeb.WebServer]
Applications[0]=UTServerAdmin.UTServerAdmin
ApplicationPaths[0]=/ServerAdmin
Applications[1]=UTServerAdmin.UTImageServer
ApplicationPaths[1]=/images
Applications[2]=ChaChaRESTStats.ChaChaRESTStats
ApplicationPaths[2]=/api/v1
DefaultApplication=0
bEnabled=True
ListenPort=<YOUR_LISTEN_PORT>
## Available Resources:
__GET__ <SERVER_PREFIX>/api/v1/map_list
__GET__ <SERVER_PREFIX>/api/v1/current_all
__GET__ <SERVER_PREFIX>/api/v1/current_game
__GET__ <SERVER_PREFIX>/api/v1/current_players
__GET__ <SERVER_PREFIX>/api/v1/default_all
__GET__ <SERVER_PREFIX>/api/v1/defaults_settings
__GET__ <SERVER_PREFIX>/api/v1/defaults_rules
__GET__ <SERVER_PREFIX>/api/v1/defaults_server
## Sample Output
__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/map_list
{"Maplist":["CTF-Gauntlet.unr","CTF-Command.unr","CTF-Coret.unr","CTF-Dreary.unr","CTF-LavaGiant.unr","CTF-November.unr"]}
__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/current_all
{"GameName":"Capture the Flag","GameClass":"Botpack.CTFGame","LevelTitle":"Lava Giant","Level":"CTF-LavaGiant","Mutators":["Botpack.FatBoy"],"player_list":[{"PlayerName":"chacha","Ping":12,"Score":0.000000,"bIsABot":false,"bIsSpectator":true,"IP":"172.16.4.101"}]}
__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/default_all
{"GameStyle":"HardCore","GameStyle":"Turbo","GameSpeed":100.000000,"AirControl":35.000000,"UseTranslocator":True,"MaxPlayers":16,"MaxSpectators":2,"bMultiWeaponStay":True,"bTournament":False,"bPlayersBalanceTeams":False,"bForceRespawn":False,"GoalTeamScore":3.000000,"TimeLimit":0,"FriendlyFireScale":0.000000,"ServerName":"Another UT Server","AdminName":"","AdminEmail":"","MOTDLine1":"","MOTDLine2":"","MOTDLine3":"","MOTDLine4":"","bWorldLog":True}

114
Run.sh Normal file
View File

@ -0,0 +1,114 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
OUTPUT_DIR="$2"
DEFAULT_CFG_FILE=UnrealTournament.ini
CFG_FILE="${3:-$DEFAULT_CFG_FILE}"
function add_iniKeyEx() {
crudini --set $OUTPUT_DIR/System/$1 $2 __$3 $4
# Warning: ugly hack with sed to allow multiple key instances + to remove space around '='
sed -i "s/[[:space:]]*__$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')[[:space:]]*=[[:space:]]*/$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')=/g" $OUTPUT_DIR/System/$1
}
# !!Warning!! section is not considered
function del_iniKeyEx() {
sed -i "/[[:space:]]*$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')[[:space:]]*=[[:space:]]*$(echo $4 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/d" $OUTPUT_DIR/System/$1
}
function add_iniKey() {
add_iniKeyEx $CFG_FILE $1 $2 $3
}
# !!Warning!! section is not considered
function del_iniKey() {
del_iniKeyEx $CFG_FILE $1 $2 $3
}
function add_ServerPackage() {
add_iniKey 'Engine.GameEngine' ServerPackages $1
add_iniKey 'XC_Engine.XC_GameEngine' ServerPackages $1
}
function del_ServerPackage() {
del_iniKey 'Engine.GameEngine' ServerPackages $1
del_iniKey 'XC_Engine.XC_GameEngine' ServerPackages $1
}
function add_ServerActors() {
add_iniKey 'Engine.GameEngine' ServerActors $1
add_iniKey 'XC_Engine.XC_GameEngine' ServerActors $1
}
function del_ServerActors() {
del_iniKey 'Engine.GameEngine' ServerActors $1
del_iniKey 'XC_Engine.XC_GameEngine' ServerActors $1
}
function install() {
rsync -a $SCRIPT_DIR/System/ $OUTPUT_DIR/System/ --exclude '.git'
echo install ok
}
function enable() {
add_iniKeyEx IpToCountry.ini UWeb.WebServer 'Applications[2]' ChaChaRESTStats.ChaChaRESTStats
add_iniKeyEx IpToCountry.ini UWeb.WebServer 'ApplicationPaths[2]' '/api/v1'
add_iniKeyEx IpToCountry.ini UWeb.WebServer 'bEnabled' True
echo enable ok
}
function disable() {
crudini --del $OUTPUT_DIR/System/$CFG_FILE UWeb.WebServer 'Applications[2]'
crudini --del $OUTPUT_DIR/System/$CFG_FILE UWeb.WebServer 'ApplicationPaths[2]'
echo disable ok
}
function show_help() {
echo
echo "Usage: $0 { install | enable | disable } <UT99_INSTALL_DIR> [<UT99_CONFIG_FILE>]"
echo
}
function check_cfg_file() {
if [ -z ${CFG_FILE} ]
then
echo "CFG_FILE is unset, setting it to $DEFAULT_CFG_FILE"
CFG_FILE=$DEFAULT_CFG_FILE
else
echo "CFG_FILE is set to '$CFG_FILE'"
fi
if [ ! -f $OUTPUT_DIR/System/$CFG_FILE ]
then
echo "$OUTPUT_DIR/System/$CFG_FILE does not exist"
show_help
exit 9999 # die with error code 9999
fi
}
function check_game_dir() {
### Check if a directory does not exist ###
if [ ! -d $OUTPUT_DIR ]
then
echo "incorrect <UT99_INSTALL_DIR>"
show_help
exit 9999 # die with error code 9999
fi
}
case "$1" in
'install')
check_game_dir
install
;;
'enable')
check_game_dir
check_cfg_file
disable
enable
;;
'disable')
check_game_dir
check_cfg_file
disable
;;
*)
show_help
exit 1
;;
esac
exit 0

View File

@ -0,0 +1,436 @@
class ChaChaRESTStats expands WebApplication;
var() class<UTServerAdminSpectator> SpectatorType;
var UTServerAdminSpectator Spectator;
var ListItem IncludeMutators;
var ListItem ExcludeMutators;
/* Usage:
[UWeb.WebServer]
Applications[X]=ChaChaRESTStats.ChaChaRESTStats
ApplicationPaths[X]=/api/v1
bEnabled=True
http://server.ip.address/api/v1/current_game
*/
event Init()
{
Super.Init();
if (SpectatorType != None)
Spectator = Level.Spawn(SpectatorType);
else
Spectator = Level.Spawn(class'UTServerAdminSpectator');
// won't change as long as the server is up
LoadMutators();
}
function LoadMutators()
{
local int NumMutatorClasses;
local string NextMutator, NextDesc;
local listitem TempItem;
local Mutator M;
local int k;
ExcludeMutators = None;
Level.GetNextIntDesc("Engine.Mutator", 0, NextMutator, NextDesc);
while( (NextMutator != "") && (NumMutatorClasses < 1024) )
{
TempItem = new(None) class'ListItem';
k = InStr(NextDesc, ",");
if (k == -1)
TempItem.Tag = NextDesc;
else
TempItem.Tag = Left(NextDesc, k);
TempItem.Data = NextMutator;
if (ExcludeMutators == None)
ExcludeMutators = TempItem;
else
ExcludeMutators.AddSortedElement(ExcludeMutators, TempItem);
NumMutatorClasses++;
Level.GetNextIntDesc("Engine.Mutator", NumMutatorClasses, NextMutator, NextDesc);
}
IncludeMutators = None;
for ( M=Level.Game.BaseMutator.NextMutator ; M!=None ; M=M.NextMutator )
{
TempItem = ExcludeMutators.DeleteElement(ExcludeMutators, String(M.Class));
if (TempItem != None)
{
if (IncludeMutators == None)
IncludeMutators = TempItem;
else
IncludeMutators.AddElement(TempItem);
}
else
log("Unknown Mutator in use: "$String(M.Class));
}
}
event Query(WebRequest Request, WebResponse Response)
{
local string tmp;
//local int i;
/* Kept in case auth needed
if(Request.Username != "test" || Request.Password != "test")
{
Response.FailAuthentication("HelloWeb");
return;
}
*/
Response.SendStandardHeaders("application/json",False);
Response.bSentText=True; //avoid header to be re-sent
switch(Request.URI)
{
case "/map_list":
Response.SendText(EncloseJSON(QueryMapList()));
break;
case "/current_all":
tmp=QueryCurrentGame();
tmp$=",";
tmp$=QueryCurrentPlayers();
Response.SendText(EncloseJSON(tmp));
break;
case "/current_game":
Response.SendText(EncloseJSON(QueryCurrentGame()));
break;
case "/current_players":
Response.SendText(EncloseJSON(QueryCurrentPlayers()));
break;
case "/default_all":
tmp=QueryDefaultsSettings();
tmp$=",";
tmp$=QueryDefaultsRules();
tmp$=",";
tmp$=QueryDefaultsServer();
Response.SendText(EncloseJSON(tmp));
break;
case "/defaults_settings":
Response.SendText(EncloseJSON(QueryDefaultsSettings()));
break;
case "/defaults_rules":
Response.SendText(EncloseJSON(QueryDefaultsRules()));
break;
case "/defaults_server":
Response.SendText(EncloseJSON(QueryDefaultsServer()));
break;
default:
Response.SendText("ERROR: Page not found or enabled.");
break;
}
}
function string EncloseJSON(String _input)
{
return "{"$_input$"}";
}
function string QueryDefaultsServer()
{
local String tmp;
tmp$="\"ServerName\":\""$class'Engine.GameReplicationInfo'.default.ServerName$"\",";
tmp$="\"AdminName\":\""$class'Engine.GameReplicationInfo'.default.AdminName$"\",";
tmp$="\"AdminEmail\":\""$class'Engine.GameReplicationInfo'.default.AdminEmail$"\",";
tmp$="\"MOTDLine1\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine1$"\",";
tmp$="\"MOTDLine2\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine2$"\",";
tmp$="\"MOTDLine3\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine3$"\",";
tmp$="\"MOTDLine4\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine4$"\",";
tmp$="\"bWorldLog\":"$Level.Game.Default.bWorldLog;
return tmp;
}
function string QueryDefaultsRules()
{
local float FriendlyFireScale;
local class<GameInfo> GameClass;
local String tmp;
GameClass = Level.Game.Class;
tmp$="\"MaxPlayers\":"$class<DeathMatchPlus>(GameClass).Default.MaxPlayers$",";
tmp$="\"MaxSpectators\":"$class<DeathMatchPlus>(GameClass).Default.MaxSpectators$",";
tmp$="\"bMultiWeaponStay\":"$class<DeathMatchPlus>(GameClass).Default.bMultiWeaponStay$",";
tmp$="\"bTournament\":"$class<DeathMatchPlus>(GameClass).Default.bTournament;
if( class<TeamGamePlus>(GameClass) != None )
{
tmp$=",\"bPlayersBalanceTeams\":"$class<TeamGamePlus>(GameClass).Default.bPlayersBalanceTeams;
}
if( class<LastManStanding>(GameClass) == None )
{
tmp$=",\"bForceRespawn\":"$class<DeathMatchPlus>(GameClass).Default.bForceRespawn;
}
if (class<DeathMatchPlus>(GameClass) != None && class<Assault>(GameClass) == None)
{
if (class<TeamGamePlus>(GameClass) != None)
{
tmp$=",\"GoalTeamScore\":"$class<TeamGamePlus>(GameClass).Default.GoalTeamScore;
}
else
{
tmp$=",\"FragLimit\":"$class<DeathMatchPlus>(GameClass).Default.FragLimit;
}
if(class<LastManStanding>(GameClass) == None)
{
tmp$=",\"TimeLimit\":"$class<DeathMatchPlus>(GameClass).Default.TimeLimit;
}
}
if( class<TeamGamePlus>(GameClass) != None &&
!ClassIsChildOf( GameClass, class'CTFGame' ) &&
!ClassIsChildOf( GameClass, class'Assault' ) )
{
tmp$=",\"MaxTeams\":"$class<TeamGamePlus>(GameClass).Default.MaxTeams;
}
if (class<TeamGamePlus>(GameClass) != None)
{
FriendlyFireScale = class<TeamGamePlus>(GameClass).Default.FriendlyFireScale * 100;
tmp$=",\"FriendlyFireScale\":"$FriendlyFireScale;
}
return tmp;
}
function string QueryDefaultsSettings()
{
local class<GameInfo> GameClass;
local int GameStyle;
local float GameSpeed, AirControl;
local String tmp;
GameClass = Level.Game.Class;
if (class<DeathMatchPlus>(GameClass).Default.bMegaSpeed == true)
GameStyle=1;
if (class<DeathMatchPlus>(GameClass).Default.bHardCoreMode == true)
GameStyle+=1;
switch (GameStyle) {
case 0:
tmp$="\"GameStyle\":\"Normal\",";
break;
case 1:
tmp$="\"GameStyle\":\"HardCore\",";
case 2:
tmp$="\"GameStyle\":\"Turbo\",";
}
GameSpeed = class<DeathMatchPlus>(GameClass).Default.GameSpeed * 100.0;
tmp$="\"GameSpeed\":"$GameSpeed$",";
AirControl = class<DeathMatchPlus>(GameClass).Default.AirControl * 100.0;
tmp$="\"AirControl\":"$AirControl$",";
tmp$="\"UseTranslocator\":"$class<DeathMatchPlus>(GameClass).Default.bUseTranslocator;
return tmp;
}
function string QueryMapList()
{
local string tmp;
local ListItem ExcludeMaps, IncludeMaps;
ReloadExcludeMaps(ExcludeMaps, String(Level.Game.Class));
ReloadIncludeMaps(ExcludeMaps, IncludeMaps, String(Level.Game.Class));
tmp$=RenderDataList("Maplist",IncludeMaps);
return tmp;
}
function string QueryCurrentGame()
{
local string tmp,LevelFullName,LevelName;
local int k;
tmp$="\"GameName\":\""$Level.Game.GameReplicationInfo.GameName$"\",";
tmp$="\"GameClass\":\""$String(Level.Game.Class)$"\",";
tmp$="\"LevelTitle\":\""$Level.Title$"\",";
LevelFullName=String(Level);
k = InStr(LevelFullName, ".");
LevelName = Left(LevelFullName, k);
tmp$="\"Level\":\""$LevelName$"\",";
if (Level.Game != None && DeathMatchPlus(Level.Game) != None &&
DeathMatchPlus(Level.Game).TimeLimit > 0.0)
{
tmp$="\"TimeLimit\":"$DeathMatchPlus(Level.Game).TimeLimit$",";
}
tmp$=RenderDataList("Mutators",IncludeMutators);
return tmp;
}
function string QueryCurrentPlayers()
{
local string tmp;
local Pawn P;
local string IP;
local bool bFirst;
tmp$="\"player_list\":[";
bFirst=True;
for (P=Level.PawnList; P!=None; P=P.NextPawn) {
if (P.bIsPlayer
&& !P.bDeleteMe
&& UTServerAdminSpectator(P) == None
&& P.PlayerReplicationInfo != None)
{
if(!bFirst)
tmp$=",";
bFirst=False;
tmp$="{";
tmp$="\"PlayerName\":\""$P.PlayerReplicationInfo.PlayerName$"\",";
tmp$="\"Ping\":"$max(P.PlayerReplicationInfo.Ping, 0)$",";
tmp$="\"Score\":"$P.PlayerReplicationInfo.Score$",";
if (P.PlayerReplicationInfo.bIsABot)
{
tmp$="\"bIsABot\":true,";
tmp$="\"bIsSpectator\":false,";
}
else
{
tmp$="\"bIsABot\":false,";
if (P.PlayerReplicationInfo.bIsSpectator)
{
tmp$="\"bIsSpectator\":true,";
}
else
{
tmp$="\"bIsSpectator\":false,";
}
}
IP = "";
if ( PlayerPawn(P) != None )
{
IP = PlayerPawn(P).GetPlayerNetworkAddress();
IP = class'InternetInfo'.static.StripPort(IP);
}
tmp$="\"IP\":\""$IP$"\"";
tmp$="}";
}
}
tmp$="]";
return tmp;
}
function string RenderDataList(String name,ListItem _list)
{
local string tmp;
local ListItem TempItem;
local bool bFirst;
tmp$="\""$name$"\":[";
bFirst = true;
for (TempItem = _list; TempItem != None; TempItem = TempItem.Next) {
if(!bFirst)
tmp$=",";
tmp$="\""$TempItem.Data$"\"";
bFirst=false;
}
tmp$="]";
return tmp;
}
function ReloadExcludeMaps(out ListItem ExcludeMaps, String GameType)
{
local class<GameInfo> GameClass;
local string FirstMap, NextMap, TestMap;
local ListItem TempItem;
GameClass = class<GameInfo>(DynamicLoadObject(GameType, class'Class'));
ExcludeMaps = None;
if(GameClass.Default.MapPrefix == "")
return;
FirstMap = Level.GetMapName(GameClass.Default.MapPrefix, "", 0);
NextMap = FirstMap;
while (!(FirstMap ~= TestMap) && FirstMap != "")
{
if(!(Left(NextMap, Len(NextMap) - 4) ~= (GameClass.Default.MapPrefix$"-tutorial")))
{
// Add the map.
TempItem = new(None) class'ListItem';
TempItem.Data = NextMap;
if(Right(NextMap, 4) ~= ".unr")
TempItem.Tag = Left(NextMap, Len(NextMap) - 4);
else
TempItem.Tag = NextMap;
if (ExcludeMaps == None)
ExcludeMaps = TempItem;
else
{
// Maplists returned by GetMapName get sorted in C++ as of the Unreal Tournament 469 patch
//ExcludeMaps.AddSortedElement(ExcludeMaps, TempItem);
ExcludeMaps.AddElement(TempItem);
}
}
NextMap = Level.GetMapName(GameClass.Default.MapPrefix, NextMap, 1);
TestMap = NextMap;
}
}
function ReloadIncludeMaps(out ListItem ExcludeMaps, out ListItem IncludeMaps, String GameType)
{
local class<GameInfo> GameClass;
local ListItem TempItem;
local int i;
GameClass = class<GameInfo>(DynamicLoadObject(GameType, class'Class'));
if(GameClass.Default.MapListType == None)
return;
if (GameClass != None)
{
for (i=0; i<ArrayCount(GameClass.Default.MapListType.Default.Maps) && GameClass.Default.MapListType.Default.Maps[i] != ""; i++)
{
// Add the map.
TempItem = ExcludeMaps.DeleteElement(ExcludeMaps, GameClass.Default.MapListType.Default.Maps[i]);
if (TempItem == None)
{
TempItem = new(None) class'ListItem';
TempItem.Data = GameClass.Default.MapListType.Default.Maps[i];
if(Right(TempItem.Data, 4) ~= ".unr")
TempItem.Tag = Left(TempItem.Data, Len(TempItem.Data) - 4);
else
TempItem.Tag = TempItem.Data;
}
else
{
if (IncludeMaps == None)
IncludeMaps = TempItem;
else
IncludeMaps.AddElement(TempItem);
}
}
}
}
defaultproperties
{
SpectatorType=Class'utserveradmin.UTServerAdminSpectator'
Spectator=None
}

2
Sources/make.bat Normal file
View File

@ -0,0 +1,2 @@
del %~dp0\..\System\ChaChaRESTStats.u
%~dp0\..\System\ucc.exe make ChaChaRESTStats

BIN
System/ChaChaRESTStats.u Normal file

Binary file not shown.