646 lines
16 KiB
Ucode
646 lines
16 KiB
Ucode
// ====================================================================
|
|
// Class: XAdmin.AdminIni
|
|
// Parent: Engine.Admin
|
|
//
|
|
// TODO: Implement <cmd> help for inline help text ?
|
|
// ====================================================================
|
|
|
|
class AdminIni extends AdminBase;
|
|
|
|
var AccessControlIni xManager;
|
|
var xAdminUser AdminUser;
|
|
var GameConfigSet ConfigSet;
|
|
|
|
var protected string NextMutators;
|
|
var protected string NextGameType;
|
|
|
|
// Localization
|
|
var localized string Msg_FinishGameEditFirst;
|
|
var localized string Msg_FinishGameRestart;
|
|
var localized string Msg_MutNeedGameEdit;
|
|
var localized string Msg_NoMutatorInUse;
|
|
var localized string Msg_NoUnusedMuts;
|
|
var localized string Msg_AddedMutator;
|
|
var localized string Msg_ErrAddingMutator;
|
|
var localized string Msg_RemovedMutator;
|
|
var localized string Msg_ErrRemovingMutator;
|
|
var localized string Msg_MapListNeedGameEdit;
|
|
var localized string Msg_MustEndGameEdit;
|
|
var localized string Msg_EditingClass;
|
|
var localized string Msg_EditFailed;
|
|
var localized string Msg_AlreadyEdited;
|
|
var localized string Msg_NotEditing;
|
|
var localized string Msg_EditingCompleted;
|
|
var localized string Msg_EditingCancelled;
|
|
var localized string Msg_NoBotGameFull;
|
|
var localized string Msg_NoAddNamedBot;
|
|
var localized string Msg_NoBotsPlaying;
|
|
var localized string Msg_GameNoSupportBots;
|
|
var localized string Msg_StatsNoBots;
|
|
var localized string Msg_SetBotNeedVal;
|
|
|
|
event Created()
|
|
{
|
|
Super.Created();
|
|
xManager = AccessControlIni(Manager);
|
|
}
|
|
|
|
// Execute an administrative console command on the server.
|
|
function DoLogin( string Username, string Password )
|
|
{
|
|
if (AdminUser == None && xManager.AdminLogin(Outer, Username, Password))
|
|
{
|
|
bAdmin = true;
|
|
AdminUser = xManager.GetLoggedAdmin(Outer);
|
|
xManager.AdminEntered(Outer, Username);
|
|
}
|
|
}
|
|
|
|
function DoLogout()
|
|
{
|
|
if (xManager.AdminLogout(Outer))
|
|
{
|
|
xManager.ReleaseConfigSet(ConfigSet, Self);
|
|
xManager.AdminExited(Outer);
|
|
bAdmin=false;
|
|
}
|
|
}
|
|
|
|
function RestartCurrentMap()
|
|
{
|
|
if (CanPerform("Mr") || CanPerform("Mc")) // Mr = MapRestart, Mc = Map Change
|
|
{
|
|
if (ConfigSet == None)
|
|
DoSwitch("?restart");
|
|
else
|
|
ClientMessage(Msg_FinishGameEditFirst);
|
|
}
|
|
}
|
|
|
|
function DoSwitch( string URL)
|
|
{
|
|
if (CanPerform("Mc"))
|
|
{
|
|
if (ConfigSet == None)
|
|
{
|
|
// Rebuild the URL based on edited game settings
|
|
if (NextGameType != "" && Level.Game.ParseOption(URL, "Game") == "")
|
|
URL=URL$"?Game="$NextGameType;
|
|
|
|
if (NextMutators != "" && Level.Game.ParseOption(URL, "Mutator") == "")
|
|
URL=URL$"?Mutator="$NextMutators;
|
|
|
|
Level.ServerTravel( URL, false );
|
|
}
|
|
else
|
|
ClientMessage(Msg_FinishGameRestart);
|
|
}
|
|
}
|
|
|
|
function GotoNextMap()
|
|
{
|
|
local string NextMap;
|
|
local MapList MyList;
|
|
local GameInfo G;
|
|
|
|
if (CanPerform("Mc"))
|
|
{
|
|
if (ConfigSet == None)
|
|
{
|
|
|
|
G = Level.Game;
|
|
if ( G.bChangeLevels && !G.bAlreadyChanged && (G.MapListType != "") )
|
|
{
|
|
// open a the nextmap actor for this game type and get the next map
|
|
G.bAlreadyChanged = true;
|
|
MyList = G.GetMapList(G.MapListType);
|
|
if (MyList != None)
|
|
{
|
|
NextMap = MyList.GetNextMap();
|
|
MyList.Destroy();
|
|
}
|
|
if ( NextMap == "" )
|
|
NextMap = GetMapName(G.MapPrefix, NextMap,1);
|
|
|
|
if ( NextMap != "" )
|
|
{
|
|
DoSwitch(NextMap);
|
|
ClientMessage(Repl(Msg_ChangingMapTo, "%NextMap%", NextMap));
|
|
return;
|
|
}
|
|
}
|
|
ClientMessage(Msg_NextMapNotFound);
|
|
Level.ServerTravel( "?Restart", false );
|
|
}
|
|
else
|
|
ClientMessage(Msg_FinishGameEditFirst);
|
|
}
|
|
}
|
|
|
|
exec function User( string Cmd, string Extra)
|
|
{
|
|
if (Cmd ~= "List") // Admin User List *mask*
|
|
{
|
|
SendUserList(Extra);
|
|
}
|
|
else if (Cmd ~= "Del") // Admin User Del *mask*
|
|
{
|
|
// TODO: Later .. need to make sure its acceptable to do so
|
|
}
|
|
else if (Cmd ~= "Logged") // List of currently logged admins
|
|
{
|
|
SendLoggedList();
|
|
}
|
|
}
|
|
|
|
// TODO: Mutators Logic should be separate from GameConfigSet since
|
|
// they are not associated to a game type.
|
|
exec function Mutators( string Cmd, string Extra)
|
|
{
|
|
local array<string> Values;
|
|
local int i;
|
|
|
|
if (CanPerform("Mu"))
|
|
{
|
|
// TODO: Separate Mutator stuff from ConfigSet ?
|
|
if (ConfigSet == None)
|
|
{
|
|
ClientMessage(Msg_MutNeedGameEdit);
|
|
return;
|
|
}
|
|
|
|
if (Cmd ~= "Used")
|
|
{
|
|
// List Used Mutators
|
|
Values = ConfigSet.GetUsedMutators();
|
|
for (i = 0; i<Values.Length; i++)
|
|
ClientMessage(i$")"@Values[i]);
|
|
if (i == 0)
|
|
ClientMessage(Msg_NoMutatorInUse);
|
|
}
|
|
else if (Cmd ~= "Unused")
|
|
{
|
|
Values = ConfigSet.GetUnusedMutators();
|
|
for (i = 0; i<Values.Length; i++)
|
|
ClientMessage(i$")"@Values[i]);
|
|
if (i == 0)
|
|
ClientMessage(Msg_NoUnusedMuts);
|
|
}
|
|
else if (Cmd ~= "Add")
|
|
{
|
|
Split(Extra, " ", Values);
|
|
for (i = 0; i<Values.Length; i++)
|
|
{
|
|
if (ConfigSet.AddMutator(Values[i]))
|
|
ClientMessage(Repl(Msg_AddedMutator, "%Mutator%", Values[i]));
|
|
else
|
|
ClientMessage(Repl(Msg_ErrAddingMutator, "%Mutator%", Values[i]));
|
|
}
|
|
}
|
|
else if (Cmd ~= "Del")
|
|
{
|
|
Split(Extra, " ", Values);
|
|
for (i = 0; i<Values.Length; i++)
|
|
{
|
|
if (ConfigSet.DelMutator(Values[i]))
|
|
ClientMessage(Repl(Msg_RemovedMutator, "%Mutator%", Values[i]));
|
|
else
|
|
ClientMessage(Repl(Msg_ErrRemovingMutator, "%Mutator%", Values[i]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function MapListCommand( string Cmd, string Extra )
|
|
{
|
|
local array<string> Values, Tmp;
|
|
local string Str;
|
|
local int i, c;
|
|
|
|
if (CanPerform("Ml"))
|
|
{
|
|
Cmd = Caps(Cmd);
|
|
if (ConfigSet == None)
|
|
{
|
|
i = MapHandler.GetGameIndex(string(Level.Game.Class));
|
|
if (Extra == "")
|
|
c = MapHandler.GetActiveList(i);
|
|
else c = int(Extra);
|
|
Str = MapHandler.GetMapListTitle(i, c);
|
|
|
|
switch (Cmd)
|
|
{
|
|
case "LIST":
|
|
Values = MapHandler.GetMapListNames(i);
|
|
SendComplexMsg(Values, Repl(Msg_AllMapLists, "%gametype%", string(Level.Game.Class)));
|
|
break;
|
|
|
|
case "USED":
|
|
Values = MapHandler.GetMapList(i, c);
|
|
SendComplexMsg(Values, Repl(Msg_MapRotationList, "%maplist%", Str));
|
|
break;
|
|
|
|
case "SWITCH":
|
|
MapHandler.ApplyMapList(i, c);
|
|
break;
|
|
|
|
default:
|
|
ClientMessage(Msg_MapListNeedGameEdit);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
i = MapHandler.GetGameIndex(ConfigSet.GetEditedClass());
|
|
|
|
switch (Cmd)
|
|
{
|
|
case "LIST":
|
|
if (Extra == "")
|
|
{
|
|
Values = ConfigSet.GetLists();
|
|
SendComplexMsg(Values, "MapLists for"@ConfigSet.GetEditedClass());
|
|
}
|
|
else
|
|
{
|
|
Values = ConfigSet.GetMaps();
|
|
Str = MapHandler.GetMapListTitle(i, int(Extra));
|
|
SendComplexMsg(Values, Repl(Msg_MapRotationList,"%maplist%",Str));
|
|
}
|
|
break;
|
|
|
|
case "USED":
|
|
Str = Repl(Msg_MapRotationList, "%maplist%", MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList));
|
|
Values = ConfigSet.GetMaps();
|
|
if (Values.Length > 0)
|
|
SendComplexMsg(Values, Str);
|
|
else ClientMessage(Msg_NoMapInRotation);
|
|
break;
|
|
|
|
case "SWITCH":
|
|
ClientMessage(Msg_MustEndGameEdit);
|
|
break;
|
|
|
|
case "EDIT":
|
|
if (Extra == "") Extra = string(ConfigSet.GetActiveList());
|
|
ConfigSet.CurrentMapList = int(extra);
|
|
ClientMessage(Repl(Msg_EditingMapList, "%List%", MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList)));
|
|
break;
|
|
|
|
case "ENDEDIT":
|
|
ConfigSet.EndEdit(bool(Extra));
|
|
break;
|
|
|
|
case "NEW":
|
|
Str = ConfigSet.GetEditedClass();
|
|
MapHandler.AddList(Str, Extra, Values);
|
|
ClientMessage(Repl(Msg_MapListAdded, "%listname%", Extra)@Str$".");
|
|
break;
|
|
|
|
case "REMOVE":
|
|
Str = MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList);
|
|
MapHandler.RemoveList(i, ConfigSet.CurrentMapList);
|
|
ClientMessage(Repl(Msg_MapListRemoved, "%listname%", Str)@ConfigSet.GetEditedClass()$".");
|
|
break;
|
|
|
|
case "ADD":
|
|
Values = ConfigSet.AddMaps(Extra);
|
|
if (Values.Length == 0)
|
|
ClientMessage(Msg_NoMapsAdded@Str$".");
|
|
else SendComplexMsg(Values, Msg_AddedMapToList @ MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList));
|
|
break;
|
|
|
|
case "DEL":
|
|
Str = MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList);
|
|
Values = ConfigSet.RemoveMaps(Extra);
|
|
if (Values.Length == 0)
|
|
ClientMessage(Msg_NoMapsRemoved@Str$".");
|
|
else SendComplexMsg(Values, Msg_RemovedFromList @ MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList));
|
|
break;
|
|
|
|
case "FIND":
|
|
Str = MapHandler.GetMapListTitle(i, ConfigSet.CurrentMapList);
|
|
Values = ConfigSet.FindMaps(Extra);
|
|
for (i = 0; i<Values.Length; i++)
|
|
{
|
|
if (Left(Values[i], 1) != "+")
|
|
{
|
|
Tmp[Tmp.Length] = Values[i];
|
|
Values.Remove(i--,1);
|
|
}
|
|
|
|
else Values[i] = Mid(Values[i],1);
|
|
}
|
|
if (Values.Length > 0)
|
|
SendComplexMsg(Values, Repl(Msg_MapIsInRotation,"%maplist%",Str));
|
|
else ClientMessage(Repl(Msg_NoMapsFound, "%maplist%", Str));
|
|
|
|
if (Tmp.Length > 0)
|
|
SendComplexMsg(Tmp, Repl(Msg_MapIsNotInRotation,"%maplist%",Str));
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
exec function Game( string Cmd, string Extra )
|
|
{
|
|
local array<string> Params;
|
|
local string LastParam, LastValue;
|
|
local int p;
|
|
|
|
if (Cmd ~= "ChangeTo") // Admin Game ChangeTo <gameclass>
|
|
{
|
|
if (CanPerform("Mt"))
|
|
{
|
|
if (ConfigSet != None)
|
|
{
|
|
ClientMessage(Msg_MustEndGameEdit);
|
|
return;
|
|
}
|
|
NextGameType = FindGameType(Extra);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!CanPerform("Ms"))
|
|
return;
|
|
|
|
if (Cmd ~= "Edit") // Admin Game Edit [CTF]
|
|
{
|
|
if (xManager.LockConfigSet(ConfigSet, Self))
|
|
{
|
|
if (ConfigSet.StartEdit(Extra))
|
|
ClientMessage(Repl(Msg_EditingClass, "%Class%", ConfigSet.GetEditedClass()));
|
|
else
|
|
{
|
|
ClientMessage(Msg_EditFailed);
|
|
xManager.ReleaseConfigSet(ConfigSet, Self);
|
|
}
|
|
}
|
|
else
|
|
ClientMessage(Msg_AlreadyEdited);
|
|
return;
|
|
}
|
|
|
|
if (ConfigSet == None)
|
|
{
|
|
ClientMessage(Msg_NotEditing);
|
|
return;
|
|
}
|
|
|
|
if (Cmd ~= "EndEdit")
|
|
{
|
|
ConfigSet.EndEdit(true);
|
|
NextMutators = ConfigSet.NextMutators;
|
|
xManager.ReleaseConfigSet(ConfigSet, Self);
|
|
ClientMessage(Msg_EditingCompleted);
|
|
}
|
|
else if (Cmd ~= "CancelEdit")
|
|
{
|
|
ConfigSet.EndEdit(false);
|
|
xManager.ReleaseConfigSet(ConfigSet, Self);
|
|
ClientMessage(Msg_EditingCancelled);
|
|
}
|
|
else if (Cmd ~= "Get")
|
|
{
|
|
if (Instr(Extra, "*") == -1 && Instr(Extra, " ") == -1)
|
|
{
|
|
LastValue = ConfigSet.GetNamedParam(Extra);
|
|
if (LastValue == "")
|
|
ClientMessage(Repl(Msg_UnknownParam,"%Value%", Extra));
|
|
else
|
|
ClientMessage(Extra@"="@LastValue);
|
|
}
|
|
else
|
|
{
|
|
Params = ConfigSet.GetMaskedParams(Extra);
|
|
if (Params.Length == 0)
|
|
ClientMessage(Msg_NoParamsFound);
|
|
else
|
|
for (p = 0; p<Params.Length; p+=2)
|
|
ClientMessage(Params[p]@"="@Params[p+1]);
|
|
}
|
|
}
|
|
else if (Cmd ~= "Set")
|
|
{
|
|
p = Instr(Extra, " ");
|
|
if (p >= 0)
|
|
{
|
|
LastParam = Left(Extra, p);
|
|
LastValue = Mid(Extra, p+1);
|
|
if (ConfigSet.SetNamedParam(LastParam, LastValue))
|
|
{
|
|
ClientMessage(Msg_ParamModified);
|
|
return;
|
|
}
|
|
}
|
|
ClientMessage(Msg_ParamNotModified);
|
|
}
|
|
}
|
|
|
|
protected function bool CanPerform(string priv)
|
|
{
|
|
return AdminUser != None && AdminUser.HasPrivilege(priv);
|
|
}
|
|
|
|
protected function SendUserList(string mask) // Todo: Mask Feature
|
|
{
|
|
local xAdminUserList uList;
|
|
local int i;
|
|
|
|
uList = AdminUser.GetManagedUsers(xManager.Groups);
|
|
for (i=0; i<uList.Count(); i++)
|
|
ClientMessage(string(i)$uList.Get(i).UserName);
|
|
}
|
|
|
|
/// TODO: Check if should send ALL logged admins or only
|
|
protected function SendLoggedList()
|
|
{
|
|
local xAdminUserList uList;
|
|
local int i;
|
|
|
|
uList = AdminUser.GetManagedUsers(xManager.Groups);
|
|
for (i=0; i<uList.Count(); i++)
|
|
if (xManager.IsLogged(uList.Get(i)))
|
|
ClientMessage(string(i)$uList.Get(i).UserName);
|
|
}
|
|
|
|
// All Bots functions are not Persistent unless Level.Game.SaveConfig()
|
|
exec function Bots( string Cmd, string Extra)
|
|
{
|
|
local int MinV, i, j;
|
|
local array<string> Params;
|
|
local array<XUtil.PlayerRecord> BotList, BotsToAdd;
|
|
local DeathMatch Game;
|
|
local Controller C, NextC;
|
|
local xBot Bot;
|
|
|
|
if (CanPerform("Mb"))
|
|
{
|
|
Game = DeathMatch(Level.Game);
|
|
if (Game == None)
|
|
{
|
|
ClientMessage(Msg_GameNoSupportBots);
|
|
return;
|
|
}
|
|
|
|
if (Game.GameStats != None)
|
|
{
|
|
ClientMessage(Msg_StatsNoBots);
|
|
return;
|
|
}
|
|
|
|
Params = SplitParams(Extra);
|
|
if (Cmd ~= "Add")
|
|
{
|
|
MinV = Game.MinPlayers;
|
|
if (MinV == 32)
|
|
{
|
|
ClientMessage(Msg_NoBotGameFull);
|
|
return;
|
|
}
|
|
|
|
if (Params.Length == 0)
|
|
{
|
|
Game.ForceAddBot();
|
|
}
|
|
else if (Params.Length == 1 && IsNumeric(Params[0]))
|
|
{
|
|
MinV = Min(32, MinV + int(Params[0]));
|
|
while (Game.MinPlayers < MinV)
|
|
Game.ForceAddBot();
|
|
}
|
|
else // Else add named bots
|
|
{
|
|
if (!Game.IsInState('MatchInProgress'))
|
|
{
|
|
ClientMessage(Msg_NoAddNamedBot);
|
|
return;
|
|
}
|
|
MakeBotsList(BotList);
|
|
// First Build a list of Bots to add
|
|
for (i = 0; i<BotList.Length; i++)
|
|
{
|
|
for (j = 0; j<Params.Length; j++)
|
|
{
|
|
if (MaskedCompare(BotList[i].DefaultName, Params[j]))
|
|
{
|
|
BotsToAdd[BotsToAdd.Length] = BotList[i];
|
|
BotList.Remove(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
MinV = Min(32, MinV + BotsToAdd.Length);
|
|
while (Game.MinPlayers<MinV)
|
|
{
|
|
if (!Game.AddBot(BotsToAdd[0].DefaultName))
|
|
break;
|
|
BotsToAdd.Remove(0, 1);
|
|
}
|
|
}
|
|
}
|
|
else if (Cmd ~= "Kill")
|
|
{
|
|
if (Game.MinPlayers == 0 || Game.NumBots == 0)
|
|
{
|
|
ClientMessage(Msg_NoBotsPlaying);
|
|
return;
|
|
}
|
|
|
|
if (Params.Length == 0) // Kill 1 random bot
|
|
{
|
|
Game.KillBots(1);
|
|
}
|
|
else if (Params.Length == 1 && IsNumeric(Params[0])) // Kill a Number of Bots
|
|
{
|
|
Game.KillBots(int(Params[0]));
|
|
}
|
|
else // Kill Named Bots
|
|
{
|
|
// TODO: Rework Loop ?
|
|
for (C = Level.ControllerList; C != None; C = NextC)
|
|
{
|
|
Bot = xBot(C);
|
|
NextC = C.NextController;
|
|
if (Bot != None && Bot.PlayerReplicationInfo != None)
|
|
{
|
|
for (i = 0; i<Params.Length; i++)
|
|
{
|
|
if (MaskedCompare(Bot.PlayerReplicationInfo.PlayerName, Params[i]))
|
|
{
|
|
Game.KillBot(C);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (Cmd ~= "Set") // Minimum number of Players
|
|
{
|
|
if (Params.Length == 1 && IsNumeric(Params[0]) && int(Params[0]) < 33)
|
|
{
|
|
Game.MinPlayers=int(Params[0]);
|
|
}
|
|
else
|
|
ClientMessage(Msg_SetBotNeedVal);
|
|
}
|
|
}
|
|
}
|
|
|
|
function MakeBotsList(out array<XUtil.PlayerRecord> BotList)
|
|
{
|
|
local xBot Bot;
|
|
local int i;
|
|
local Controller C;
|
|
|
|
// Get Full Bot List
|
|
class'XUtil'.static.GetPlayerList(BotList);
|
|
// Filter out Playing Bots
|
|
for (C = Level.ControllerList; C != None; C = C.NextController)
|
|
{
|
|
Bot = xBot(C);
|
|
if (Bot != None && Bot.PlayerReplicationInfo != None)
|
|
{
|
|
for (i = 0; i<BotList.Length; i++)
|
|
{
|
|
if (Bot.PlayerReplicationInfo.CharacterName == BotList[i].DefaultName)
|
|
{
|
|
BotList.Remove(i,1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
Msg_NoBotGameFull="Cannot add a bot, game is full."
|
|
Msg_NoAddNamedBot="Can only add named bots once the match has started"
|
|
Msg_NoBotsPlaying="No bots are currently playing"
|
|
Msg_GameNoSupportBots="The current Game Type does not support Bots"
|
|
Msg_StatsNoBots="Cannot control bots when Worlds Stats are enabled"
|
|
Msg_FinishGameEditFirst="You must finish your Game Edit before restarting the map"
|
|
Msg_FinishGameRestart="You must finish your Game Edit before changing or restarting the map"
|
|
Msg_MutNeedGameEdit="You must use 'Game Edit' command before 'Mutators' commands"
|
|
Msg_NoMutatorInUse="No Mutators in use"
|
|
Msg_NoUnusedMuts="Found no unused mutators"
|
|
Msg_AddedMutator="Added '%Mutator%' to used mutator list."
|
|
Msg_ErrAddingMutator="Error Adding '%Mutator%'To Used Mutator List"
|
|
Msg_RemovedMutator="Removed '%Mutator%' From Used Mutator List"
|
|
Msg_ErrRemovingMutator="Error Removing '%Mutator%' from used mutator List"
|
|
Msg_MapListNeedGameEdit="You must use 'Game Edit' command before 'MapList' command"
|
|
Msg_MustEndGameEdit="You must end your Game Edit first"
|
|
Msg_EditingClass="Editing %Class%"
|
|
Msg_EditFailed="Failed Starting To Edit"
|
|
Msg_AlreadyEdited="Game Already being edited by Someone Else"
|
|
Msg_NotEditing="You are not editing Game Settings, use 'Game Edit' first"
|
|
Msg_EditingCompleted="Editing Completed"
|
|
Msg_EditingCancelled="Editing Cancelled"
|
|
Msg_SetBotNeedVal="This command requires a numeric value between 0 and 32"
|
|
}
|