Make bots first-class players.
- Bots have their own Clients, with unique ClientIDs - Hosts can set bot team/color in the lobby - Bots are kicked when switching to a smaller map without enough slots - Order validator assumes that only client 0 has permission to issue bot orders
This commit is contained in:
@@ -34,8 +34,7 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
public string FirstEmptySlot()
|
public string FirstEmptySlot()
|
||||||
{
|
{
|
||||||
return Slots.FirstOrDefault(s => !s.Value.Closed && ClientInSlot(s.Key) == null
|
return Slots.FirstOrDefault(s => !s.Value.Closed && ClientInSlot(s.Key) == null).Key;
|
||||||
&& s.Value.Bot == null).Key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ClientState
|
public enum ClientState
|
||||||
@@ -55,12 +54,12 @@ namespace OpenRA.Network
|
|||||||
public ClientState State;
|
public ClientState State;
|
||||||
public int Team;
|
public int Team;
|
||||||
public string Slot; // slot ID, or null for observer
|
public string Slot; // slot ID, or null for observer
|
||||||
|
public string Bot; // Bot type, null for real clients
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Slot
|
public class Slot
|
||||||
{
|
{
|
||||||
public string PlayerReference; // playerReference to bind against.
|
public string PlayerReference; // playerReference to bind against.
|
||||||
public string Bot; // trait name of the bot to initialize in this slot, or null otherwise.
|
|
||||||
public bool Closed; // host has explicitly closed this slot.
|
public bool Closed; // host has explicitly closed this slot.
|
||||||
|
|
||||||
public bool AllowBots;
|
public bool AllowBots;
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ namespace OpenRA.Network
|
|||||||
static Player FindPlayerByClient(this World world, Session.Client c)
|
static Player FindPlayerByClient(this World world, Session.Client c)
|
||||||
{
|
{
|
||||||
/* todo: this is still a hack.
|
/* todo: this is still a hack.
|
||||||
* the cases we're trying to avoid are the extra players on the host's client -- Neutral, other MapPlayers,
|
* the cases we're trying to avoid are the extra players on the host's client -- Neutral, other MapPlayers,..*/
|
||||||
* bots,.. */
|
|
||||||
return world.Players.FirstOrDefault(
|
return world.Players.FirstOrDefault(
|
||||||
p => (p.ClientIndex == c.Index && p.PlayerRef.Playable && !p.IsBot));
|
p => (p.ClientIndex == c.Index && p.PlayerRef.Playable));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
|
public static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ namespace OpenRA
|
|||||||
PlayerRef = pr;
|
PlayerRef = pr;
|
||||||
string botType = null;
|
string botType = null;
|
||||||
|
|
||||||
|
// Real player or host-created bot
|
||||||
if (client != null)
|
if (client != null)
|
||||||
{
|
{
|
||||||
ClientIndex = client.Index;
|
ClientIndex = client.Index;
|
||||||
ColorRamp = client.ColorRamp;
|
ColorRamp = client.ColorRamp;
|
||||||
PlayerName = client.Name;
|
PlayerName = client.Name;
|
||||||
|
botType = client.Bot;
|
||||||
|
|
||||||
Country = world.GetCountries()
|
Country = world.GetCountries()
|
||||||
.FirstOrDefault(c => client.Country == c.Race)
|
.FirstOrDefault(c => client.Country == c.Race)
|
||||||
@@ -58,36 +60,21 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Map player or bot
|
// Map player
|
||||||
ClientIndex = 0; /* it's a map player, "owned" by host */
|
ClientIndex = 0; /* it's a map player, "owned" by host */
|
||||||
ColorRamp = pr.ColorRamp;
|
ColorRamp = pr.ColorRamp;
|
||||||
PlayerName = pr.Name;
|
PlayerName = pr.Name;
|
||||||
NonCombatant = pr.NonCombatant;
|
NonCombatant = pr.NonCombatant;
|
||||||
IsBot = pr.Bot != null;
|
|
||||||
botType = pr.Bot;
|
botType = pr.Bot;
|
||||||
|
|
||||||
Country = world.GetCountries()
|
Country = world.GetCountries()
|
||||||
.FirstOrDefault(c => pr.Race == c.Race)
|
.FirstOrDefault(c => pr.Race == c.Race)
|
||||||
?? world.GetCountries().Random(world.SharedRandom);
|
?? world.GetCountries().Random(world.SharedRandom);
|
||||||
|
|
||||||
// Multiplayer bot
|
|
||||||
if (slot != null && slot.Bot != null)
|
|
||||||
{
|
|
||||||
IsBot = true;
|
|
||||||
botType = slot.Bot;
|
|
||||||
PlayerName = slot.Bot;
|
|
||||||
|
|
||||||
// pick a random color for the bot
|
|
||||||
var hue = (byte)world.SharedRandom.Next(255);
|
|
||||||
var sat = (byte)world.SharedRandom.Next(255);
|
|
||||||
var lum = (byte)world.SharedRandom.Next(51,255);
|
|
||||||
ColorRamp = new ColorRamp(hue, sat, lum, 10);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerActor = world.CreateActor("Player", new TypeDictionary { new OwnerInit(this) });
|
PlayerActor = world.CreateActor("Player", new TypeDictionary { new OwnerInit(this) });
|
||||||
|
|
||||||
// Enable the bot logic
|
// Enable the bot logic on the host
|
||||||
|
IsBot = botType != null;
|
||||||
if (IsBot && Game.IsHost)
|
if (IsBot && Game.IsHost)
|
||||||
{
|
{
|
||||||
var logic = PlayerActor.TraitsImplementing<IBot>()
|
var logic = PlayerActor.TraitsImplementing<IBot>()
|
||||||
|
|||||||
@@ -129,10 +129,11 @@ namespace OpenRA.Server
|
|||||||
* for manual spawnpoint choosing.
|
* for manual spawnpoint choosing.
|
||||||
* - 256 max players is a dirty hack
|
* - 256 max players is a dirty hack
|
||||||
*/
|
*/
|
||||||
int ChooseFreePlayerIndex()
|
public int ChooseFreePlayerIndex()
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 256; i++)
|
for (var i = 0; i < 256; i++)
|
||||||
if (conns.All(c => c.PlayerIndex != i) && preConns.All(c => c.PlayerIndex != i))
|
if (conns.All(c => c.PlayerIndex != i) && preConns.All(c => c.PlayerIndex != i)
|
||||||
|
&& lobbyInfo.Clients.All(c => c.Index != i))
|
||||||
return i;
|
return i;
|
||||||
|
|
||||||
throw new InvalidOperationException("Already got 256 players");
|
throw new InvalidOperationException("Already got 256 players");
|
||||||
|
|||||||
@@ -18,10 +18,18 @@ namespace OpenRA.Traits
|
|||||||
{
|
{
|
||||||
public bool OrderValidation(OrderManager orderManager, World world, int clientId, Order order)
|
public bool OrderValidation(OrderManager orderManager, World world, int clientId, Order order)
|
||||||
{
|
{
|
||||||
// Drop exploiting orders
|
if (order.Subject == null || order.Subject.Owner == null)
|
||||||
if (order.Subject != null && order.Subject.Owner.ClientIndex != clientId)
|
return true;
|
||||||
|
|
||||||
|
var subjectClient = order.Subject.Owner.ClientIndex;
|
||||||
|
|
||||||
|
// Hack: Assumes bots always run on clientId 0.
|
||||||
|
var isBotOrder = orderManager.LobbyInfo.Clients[subjectClient].Bot != null && clientId == 0;
|
||||||
|
|
||||||
|
// Drop exploiting orders
|
||||||
|
if (subjectClient != clientId && !isBotOrder)
|
||||||
{
|
{
|
||||||
Game.Debug("Detected exploit order from {0}: {1}".F(clientId, order.OrderString));
|
Game.Debug("Detected exploit order from client {0}: {1}".F(clientId, order.OrderString));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
{
|
{
|
||||||
public class CncLobbyLogic
|
public class CncLobbyLogic
|
||||||
{
|
{
|
||||||
Widget LocalPlayerTemplate, RemotePlayerTemplate, EmptySlotTemplate, BotTemplate,
|
Widget LocalPlayerTemplate, RemotePlayerTemplate, EmptySlotTemplate,
|
||||||
LocalSpectatorTemplate, RemoteSpectatorTemplate, NewSpectatorTemplate;
|
LocalSpectatorTemplate, RemoteSpectatorTemplate, NewSpectatorTemplate;
|
||||||
ScrollPanelWidget chatPanel;
|
ScrollPanelWidget chatPanel;
|
||||||
Widget chatTemplate;
|
Widget chatTemplate;
|
||||||
|
|
||||||
@@ -108,7 +108,6 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
LocalPlayerTemplate = Players.GetWidget("TEMPLATE_LOCAL");
|
LocalPlayerTemplate = Players.GetWidget("TEMPLATE_LOCAL");
|
||||||
RemotePlayerTemplate = Players.GetWidget("TEMPLATE_REMOTE");
|
RemotePlayerTemplate = Players.GetWidget("TEMPLATE_REMOTE");
|
||||||
EmptySlotTemplate = Players.GetWidget("TEMPLATE_EMPTY");
|
EmptySlotTemplate = Players.GetWidget("TEMPLATE_EMPTY");
|
||||||
BotTemplate = Players.GetWidget("TEMPLATE_BOT");
|
|
||||||
LocalSpectatorTemplate = Players.GetWidget("TEMPLATE_LOCAL_SPECTATOR");
|
LocalSpectatorTemplate = Players.GetWidget("TEMPLATE_LOCAL_SPECTATOR");
|
||||||
RemoteSpectatorTemplate = Players.GetWidget("TEMPLATE_REMOTE_SPECTATOR");
|
RemoteSpectatorTemplate = Players.GetWidget("TEMPLATE_REMOTE_SPECTATOR");
|
||||||
NewSpectatorTemplate = Players.GetWidget("TEMPLATE_NEW_SPECTATOR");
|
NewSpectatorTemplate = Players.GetWidget("TEMPLATE_NEW_SPECTATOR");
|
||||||
@@ -150,7 +149,8 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
return sc;
|
return sc;
|
||||||
};
|
};
|
||||||
|
|
||||||
CountryNames = Rules.Info["world"].Traits.WithInterface<OpenRA.Traits.CountryInfo>().ToDictionary(a => a.Race, a => a.Name);
|
CountryNames = Rules.Info["world"].Traits.WithInterface<OpenRA.Traits.CountryInfo>()
|
||||||
|
.ToDictionary(a => a.Race, a => a.Name);
|
||||||
CountryNames.Add("random", "Random");
|
CountryNames.Add("random", "Random");
|
||||||
|
|
||||||
var mapButton = lobby.GetWidget<ButtonWidget>("CHANGEMAP_BUTTON");
|
var mapButton = lobby.GetWidget<ButtonWidget>("CHANGEMAP_BUTTON");
|
||||||
@@ -296,25 +296,27 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShowSlotDropDown(DropDownButtonWidget dropdown, Session.Slot slot)
|
bool ShowSlotDropDown(DropDownButtonWidget dropdown, Session.Slot slot, Session.Client client)
|
||||||
{
|
{
|
||||||
var options = new List<SlotDropDownOption>()
|
var options = new List<SlotDropDownOption>()
|
||||||
{
|
{
|
||||||
new SlotDropDownOption("Open", "slot_open "+slot.PlayerReference, () => (!slot.Closed && slot.Bot == null)),
|
new SlotDropDownOption("Open", "slot_open "+slot.PlayerReference, () => (!slot.Closed && client == null)),
|
||||||
new SlotDropDownOption("Closed", "slot_close "+slot.PlayerReference, () => slot.Closed)
|
new SlotDropDownOption("Closed", "slot_close "+slot.PlayerReference, () => slot.Closed)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (slot.AllowBots)
|
if (slot.AllowBots)
|
||||||
foreach (var b in Rules.Info["player"].Traits.WithInterface<IBotInfo>().Select(t => t.Name))
|
foreach (var b in Rules.Info["player"].Traits.WithInterface<IBotInfo>().Select(t => t.Name))
|
||||||
{
|
{
|
||||||
var bot = b;
|
var bot = b;
|
||||||
options.Add(new SlotDropDownOption("Bot: {0}".F(bot), "slot_bot {0} {1}".F(slot.PlayerReference, bot), () => slot.Bot == bot));
|
options.Add(new SlotDropDownOption("Bot: {0}".F(bot),
|
||||||
|
"slot_bot {0} {1}".F(slot.PlayerReference, bot),
|
||||||
|
() => client != null && client.Bot == bot));
|
||||||
}
|
}
|
||||||
|
|
||||||
Func<SlotDropDownOption, ScrollItemWidget, ScrollItemWidget> setupItem = (o, itemTemplate) =>
|
Func<SlotDropDownOption, ScrollItemWidget, ScrollItemWidget> setupItem = (o, itemTemplate) =>
|
||||||
{
|
{
|
||||||
var item = ScrollItemWidget.Setup(itemTemplate,
|
var item = ScrollItemWidget.Setup(itemTemplate,
|
||||||
o.Selected,
|
o.Selected,
|
||||||
() => orderManager.IssueOrder(Order.Command(o.Order)));
|
() => orderManager.IssueOrder(Order.Command(o.Order)));
|
||||||
item.GetWidget<LabelWidget>("LABEL").GetText = () => o.Title;
|
item.GetWidget<LabelWidget>("LABEL").GetText = () => o.Title;
|
||||||
return item;
|
return item;
|
||||||
@@ -329,7 +331,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (race, itemTemplate) =>
|
Func<string, ScrollItemWidget, ScrollItemWidget> setupItem = (race, itemTemplate) =>
|
||||||
{
|
{
|
||||||
var item = ScrollItemWidget.Setup(itemTemplate,
|
var item = ScrollItemWidget.Setup(itemTemplate,
|
||||||
() => client.Country == race,
|
() => client.Country == race,
|
||||||
() => orderManager.IssueOrder(Order.Command("race {0} {1}".F(client.Index, race))));
|
() => orderManager.IssueOrder(Order.Command("race {0} {1}".F(client.Index, race))));
|
||||||
item.GetWidget<LabelWidget>("LABEL").GetText = () => CountryNames[race];
|
item.GetWidget<LabelWidget>("LABEL").GetText = () => CountryNames[race];
|
||||||
var flag = item.GetWidget<ImageWidget>("FLAG");
|
var flag = item.GetWidget<ImageWidget>("FLAG");
|
||||||
@@ -347,7 +349,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
Func<int, ScrollItemWidget, ScrollItemWidget> setupItem = (ii, itemTemplate) =>
|
Func<int, ScrollItemWidget, ScrollItemWidget> setupItem = (ii, itemTemplate) =>
|
||||||
{
|
{
|
||||||
var item = ScrollItemWidget.Setup(itemTemplate,
|
var item = ScrollItemWidget.Setup(itemTemplate,
|
||||||
() => client.Team == ii,
|
() => client.Team == ii,
|
||||||
() => orderManager.IssueOrder(Order.Command("team {0} {1}".F(client.Index, ii))));
|
() => orderManager.IssueOrder(Order.Command("team {0} {1}".F(client.Index, ii))));
|
||||||
item.GetWidget<LabelWidget>("LABEL").GetText = () => ii == 0 ? "-" : ii.ToString();
|
item.GetWidget<LabelWidget>("LABEL").GetText = () => ii == 0 ? "-" : ii.ToString();
|
||||||
return item;
|
return item;
|
||||||
@@ -377,7 +379,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
{
|
{
|
||||||
{ "onSelect", onSelect },
|
{ "onSelect", onSelect },
|
||||||
{ "onChange", onChange },
|
{ "onChange", onChange },
|
||||||
{ "initialRamp", orderManager.LocalClient.ColorRamp }
|
{ "initialRamp", client.ColorRamp }
|
||||||
});
|
});
|
||||||
|
|
||||||
color.AttachPanel(colorChooser);
|
color.AttachPanel(colorChooser);
|
||||||
@@ -398,7 +400,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
Widget template;
|
Widget template;
|
||||||
|
|
||||||
// Empty slot
|
// Empty slot
|
||||||
if (client == null && slot.Bot == null)
|
if (client == null)
|
||||||
{
|
{
|
||||||
template = EmptySlotTemplate.Clone();
|
template = EmptySlotTemplate.Clone();
|
||||||
Func<string> getText = () => slot.Closed ? "Closed" : "Open";
|
Func<string> getText = () => slot.Closed ? "Closed" : "Open";
|
||||||
@@ -408,7 +410,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
var name = template.GetWidget<DropDownButtonWidget>("NAME_HOST");
|
var name = template.GetWidget<DropDownButtonWidget>("NAME_HOST");
|
||||||
name.IsVisible = () => true;
|
name.IsVisible = () => true;
|
||||||
name.GetText = getText;
|
name.GetText = getText;
|
||||||
name.OnMouseDown = _ => ShowSlotDropDown(name, slot);
|
name.OnMouseDown = _ => ShowSlotDropDown(name, slot, client);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -421,51 +423,43 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
if (join != null)
|
if (join != null)
|
||||||
{
|
{
|
||||||
join.OnMouseUp = _ => { orderManager.IssueOrder(Order.Command("slot " + key)); return true; };
|
join.OnMouseUp = _ => { orderManager.IssueOrder(Order.Command("slot " + key)); return true; };
|
||||||
join.IsVisible = () => !slot.Closed && slot.Bot == null && orderManager.LocalClient.State != Session.ClientState.Ready;
|
join.IsVisible = () => !slot.Closed && orderManager.LocalClient.State != Session.ClientState.Ready;
|
||||||
}
|
|
||||||
}
|
|
||||||
// Bot
|
|
||||||
else if (client == null && slot.Bot != null)
|
|
||||||
{
|
|
||||||
template = BotTemplate.Clone();
|
|
||||||
Func<string> getText = () => slot.Bot;
|
|
||||||
|
|
||||||
if (Game.IsHost)
|
|
||||||
{
|
|
||||||
var name = template.GetWidget<DropDownButtonWidget>("NAME_HOST");
|
|
||||||
name.IsVisible = () => true;
|
|
||||||
name.GetText = getText;
|
|
||||||
name.OnMouseDown = _ => ShowSlotDropDown(name, slot);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var name = template.GetWidget<LabelWidget>("NAME");
|
|
||||||
name.IsVisible = () => true;
|
|
||||||
name.GetText = getText;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Editable player in slot
|
// Editable player in slot
|
||||||
else if (client.Index == orderManager.LocalClient.Index && client.State != Session.ClientState.Ready)
|
else if ((client.Index == orderManager.LocalClient.Index && client.State != Session.ClientState.Ready) ||
|
||||||
|
(client.Bot != null && Game.IsHost))
|
||||||
{
|
{
|
||||||
template = LocalPlayerTemplate.Clone();
|
template = LocalPlayerTemplate.Clone();
|
||||||
var name = template.GetWidget<TextFieldWidget>("NAME");
|
if (client.Bot != null)
|
||||||
name.Text = client.Name;
|
|
||||||
name.OnEnterKey = () =>
|
|
||||||
{
|
{
|
||||||
name.Text = name.Text.Trim();
|
var name = template.GetWidget<DropDownButtonWidget>("BOT_DROPDOWN");
|
||||||
if (name.Text.Length == 0)
|
name.IsVisible = () => true;
|
||||||
name.Text = client.Name;
|
name.GetText = () => client.Name;
|
||||||
|
name.OnMouseDown = _ => ShowSlotDropDown(name, slot, client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var name = template.GetWidget<TextFieldWidget>("NAME");
|
||||||
|
name.IsVisible = () => true;
|
||||||
|
name.Text = client.Name;
|
||||||
|
name.OnEnterKey = () =>
|
||||||
|
{
|
||||||
|
name.Text = name.Text.Trim();
|
||||||
|
if (name.Text.Length == 0)
|
||||||
|
name.Text = client.Name;
|
||||||
|
|
||||||
name.LoseFocus();
|
name.LoseFocus();
|
||||||
if (name.Text == client.Name)
|
if (name.Text == client.Name)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
orderManager.IssueOrder(Order.Command("name " + name.Text));
|
||||||
|
Game.Settings.Player.Name = name.Text;
|
||||||
|
Game.Settings.Save();
|
||||||
return true;
|
return true;
|
||||||
|
};
|
||||||
orderManager.IssueOrder(Order.Command("name " + name.Text));
|
name.OnLoseFocus = () => name.OnEnterKey();
|
||||||
Game.Settings.Player.Name = name.Text;
|
}
|
||||||
Game.Settings.Save();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
name.OnLoseFocus = () => name.OnEnterKey();
|
|
||||||
|
|
||||||
var color = template.GetWidget<DropDownButtonWidget>("COLOR");
|
var color = template.GetWidget<DropDownButtonWidget>("COLOR");
|
||||||
color.IsDisabled = () => slot.LockColor;
|
color.IsDisabled = () => slot.LockColor;
|
||||||
@@ -485,11 +479,12 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
factionflag.GetImageCollection = () => "flags";
|
factionflag.GetImageCollection = () => "flags";
|
||||||
|
|
||||||
var team = template.GetWidget<DropDownButtonWidget>("TEAM");
|
var team = template.GetWidget<DropDownButtonWidget>("TEAM");
|
||||||
team.IsDisabled = () => slot.LockTeam;
|
team.IsDisabled = () => slot.LockTeam || client.Bot != null;
|
||||||
team.OnMouseDown = _ => { if (slot.LockTeam) return true; return ShowTeamDropDown(team, client); };
|
team.OnMouseDown = _ => { if (team.IsDisabled()) return true; return ShowTeamDropDown(team, client); };
|
||||||
team.GetText = () => (client.Team == 0) ? "-" : client.Team.ToString();
|
team.GetText = () => (client.Team == 0) ? "-" : client.Team.ToString();
|
||||||
|
|
||||||
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
||||||
|
status.IsVisible = () => client.Bot == null;
|
||||||
status.IsChecked = () => client.State == Session.ClientState.Ready;
|
status.IsChecked = () => client.State == Session.ClientState.Ready;
|
||||||
status.OnClick += CycleReady;
|
status.OnClick += CycleReady;
|
||||||
}
|
}
|
||||||
@@ -513,6 +508,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
|
|
||||||
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
||||||
status.IsChecked = () => client.State == Session.ClientState.Ready;
|
status.IsChecked = () => client.State == Session.ClientState.Ready;
|
||||||
|
status.IsVisible = () => client.Bot == null;
|
||||||
if (client.Index == orderManager.LocalClient.Index)
|
if (client.Index == orderManager.LocalClient.Index)
|
||||||
status.OnClick += CycleReady;
|
status.OnClick += CycleReady;
|
||||||
|
|
||||||
@@ -661,6 +657,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
|
|
||||||
// Set the initial state
|
// Set the initial state
|
||||||
updateSliders();
|
updateSliders();
|
||||||
|
onChange(ramp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic
|
|||||||
panel.GetWidget<LabelWidget>("MAP_TITLE").GetText =
|
panel.GetWidget<LabelWidget>("MAP_TITLE").GetText =
|
||||||
() => currentMap != null ? currentMap.Title : "(Unknown Map)";
|
() => currentMap != null ? currentMap.Title : "(Unknown Map)";
|
||||||
|
|
||||||
var players = currentSummary.LobbyInfo.Slots.Count(s => currentSummary.LobbyInfo.ClientInSlot(s.Key) != null || s.Value.Bot != null);
|
var players = currentSummary.LobbyInfo.Slots
|
||||||
|
.Count(s => currentSummary.LobbyInfo.ClientInSlot(s.Key) != null);
|
||||||
panel.GetWidget<LabelWidget>("PLAYERS").GetText = () => players.ToString();
|
panel.GetWidget<LabelWidget>("PLAYERS").GetText = () => players.ToString();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ namespace OpenRA.Mods.RA
|
|||||||
foreach (var kv in w.LobbyInfo.Slots)
|
foreach (var kv in w.LobbyInfo.Slots)
|
||||||
{
|
{
|
||||||
var client = w.LobbyInfo.ClientInSlot(kv.Key);
|
var client = w.LobbyInfo.ClientInSlot(kv.Key);
|
||||||
if (client == null && kv.Value.Bot == null)
|
if (client == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var player = new Player(w, client, kv.Value, w.Map.Players[kv.Value.PlayerReference]);
|
var player = new Player(w, client, kv.Value, w.Map.Players[kv.Value.PlayerReference]);
|
||||||
w.AddPlayer(player);
|
w.AddPlayer(player);
|
||||||
|
|
||||||
if (client != null && client.Index == Game.LocalClientId)
|
if (client.Index == Game.LocalClientId)
|
||||||
w.SetLocalPlayer(player.InternalName);
|
w.SetLocalPlayer(player.InternalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
}
|
}
|
||||||
var slot = server.lobbyInfo.Slots[s];
|
var slot = server.lobbyInfo.Slots[s];
|
||||||
|
|
||||||
if (slot.Closed || slot.Bot != null ||
|
if (slot.Closed || server.lobbyInfo.ClientInSlot(s) != null)
|
||||||
server.lobbyInfo.ClientInSlot(s) != null)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
client.Slot = s;
|
client.Slot = s;
|
||||||
@@ -119,17 +118,20 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
var occupant = server.lobbyInfo.ClientInSlot(s);
|
var occupant = server.lobbyInfo.ClientInSlot(s);
|
||||||
if (occupant != null)
|
if (occupant != null)
|
||||||
{
|
{
|
||||||
var occupantConn = server.conns.FirstOrDefault( c => c.PlayerIndex == occupant.Index );
|
if (occupant.Bot != null)
|
||||||
if (occupantConn != null)
|
server.lobbyInfo.Clients.Remove(occupant);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host");
|
var occupantConn = server.conns.FirstOrDefault( c => c.PlayerIndex == occupant.Index );
|
||||||
server.DropClient(occupantConn);
|
if (occupantConn != null)
|
||||||
|
{
|
||||||
|
server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host");
|
||||||
|
server.DropClient(occupantConn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var slot = server.lobbyInfo.Slots[s];
|
|
||||||
slot.Closed = true;
|
|
||||||
slot.Bot = null;
|
|
||||||
|
|
||||||
|
server.lobbyInfo.Slots[s].Closed = true;
|
||||||
server.SyncLobbyInfo();
|
server.SyncLobbyInfo();
|
||||||
return true;
|
return true;
|
||||||
}},
|
}},
|
||||||
@@ -150,7 +152,11 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
|
|
||||||
var slot = server.lobbyInfo.Slots[s];
|
var slot = server.lobbyInfo.Slots[s];
|
||||||
slot.Closed = false;
|
slot.Closed = false;
|
||||||
slot.Bot = null;
|
|
||||||
|
// Slot may have a bot in it
|
||||||
|
var occupant = server.lobbyInfo.ClientInSlot(s);
|
||||||
|
if (occupant != null && occupant.Bot != null)
|
||||||
|
server.lobbyInfo.Clients.Remove(occupant);
|
||||||
|
|
||||||
server.SyncLobbyInfo();
|
server.SyncLobbyInfo();
|
||||||
return true;
|
return true;
|
||||||
@@ -178,10 +184,30 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var botType = string.Join(" ", parts.Skip(1).ToArray() );
|
||||||
var slot = server.lobbyInfo.Slots[parts[0]];
|
var slot = server.lobbyInfo.Slots[parts[0]];
|
||||||
slot.Bot = string.Join(" ", parts.Skip(1).ToArray() );
|
|
||||||
slot.Closed = false;
|
slot.Closed = false;
|
||||||
|
|
||||||
|
var bot = new Session.Client()
|
||||||
|
{
|
||||||
|
Index = server.ChooseFreePlayerIndex(),
|
||||||
|
Name = botType,
|
||||||
|
Bot = botType,
|
||||||
|
Slot = parts[0],
|
||||||
|
Country = "random",
|
||||||
|
SpawnPoint = 0,
|
||||||
|
Team = 0,
|
||||||
|
State = Session.ClientState.NotReady
|
||||||
|
};
|
||||||
|
|
||||||
|
// pick a random color for the bot
|
||||||
|
var hue = (byte)Game.CosmeticRandom.Next(255);
|
||||||
|
var sat = (byte)Game.CosmeticRandom.Next(255);
|
||||||
|
var lum = (byte)Game.CosmeticRandom.Next(51,255);
|
||||||
|
bot.ColorRamp = new ColorRamp(hue, sat, lum, 10);
|
||||||
|
|
||||||
|
S.SyncClientToPlayerReference(client, server.Map.Players[parts[0]]);
|
||||||
|
server.lobbyInfo.Clients.Add(bot);
|
||||||
server.SyncLobbyInfo();
|
server.SyncLobbyInfo();
|
||||||
return true;
|
return true;
|
||||||
}},
|
}},
|
||||||
@@ -193,20 +219,29 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
server.SendChatTo( conn, "Only the host can change the map" );
|
server.SendChatTo( conn, "Only the host can change the map" );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
server.lobbyInfo.GlobalSettings.Map = s;
|
server.lobbyInfo.GlobalSettings.Map = s;
|
||||||
|
var oldSlots = server.lobbyInfo.Slots.Keys.ToArray();
|
||||||
LoadMap(server);
|
LoadMap(server);
|
||||||
|
|
||||||
// Reassign players into slots
|
// Reassign players into new slots based on their old slots:
|
||||||
|
// - Observers remain as observers
|
||||||
|
// - Players who now lack a slot are made observers
|
||||||
|
// - Bots who now lack a slot are dropped
|
||||||
|
var slots = server.lobbyInfo.Slots.Keys.ToArray();
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach(var c in server.lobbyInfo.Clients)
|
foreach (var os in oldSlots)
|
||||||
{
|
{
|
||||||
|
var c = server.lobbyInfo.ClientInSlot(os);
|
||||||
|
if (c == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
c.SpawnPoint = 0;
|
c.SpawnPoint = 0;
|
||||||
c.State = Session.ClientState.NotReady;
|
c.State = Session.ClientState.NotReady;
|
||||||
c.Slot = c.Slot == null || i >= server.lobbyInfo.Slots.Count ?
|
c.Slot = i < slots.Length ? slots[i++] : null;
|
||||||
null : server.lobbyInfo.Slots.ElementAt(i++).Key;
|
|
||||||
|
|
||||||
if (c.Slot != null)
|
if (c.Slot != null)
|
||||||
S.SyncClientToPlayerReference(c, server.Map.Players[c.Slot]);
|
S.SyncClientToPlayerReference(c, server.Map.Players[c.Slot]);
|
||||||
|
else if (c.Bot != null)
|
||||||
|
server.lobbyInfo.Clients.Remove(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.SyncLobbyInfo();
|
server.SyncLobbyInfo();
|
||||||
@@ -282,7 +317,6 @@ namespace OpenRA.Mods.RA.Server
|
|||||||
return new Session.Slot
|
return new Session.Slot
|
||||||
{
|
{
|
||||||
PlayerReference = pr.Name,
|
PlayerReference = pr.Name,
|
||||||
Bot = null,
|
|
||||||
Closed = false,
|
Closed = false,
|
||||||
AllowBots = pr.AllowBots,
|
AllowBots = pr.AllowBots,
|
||||||
LockRace = pr.LockRace,
|
LockRace = pr.LockRace,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
readonly OrderManager orderManager;
|
readonly OrderManager orderManager;
|
||||||
readonly WorldRenderer worldRenderer;
|
readonly WorldRenderer worldRenderer;
|
||||||
[ObjectCreator.UseCtor]
|
[ObjectCreator.UseCtor]
|
||||||
internal LobbyLogic( [ObjectCreator.Param( "widget" )] Widget lobby, [ObjectCreator.Param] OrderManager orderManager, [ObjectCreator.Param] WorldRenderer worldRenderer)
|
internal LobbyLogic([ObjectCreator.Param( "widget" )] Widget lobby,
|
||||||
|
[ObjectCreator.Param] OrderManager orderManager,
|
||||||
|
[ObjectCreator.Param] WorldRenderer worldRenderer)
|
||||||
{
|
{
|
||||||
this.orderManager = orderManager;
|
this.orderManager = orderManager;
|
||||||
this.worldRenderer = worldRenderer;
|
this.worldRenderer = worldRenderer;
|
||||||
@@ -93,13 +95,18 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
return sc;
|
return sc;
|
||||||
};
|
};
|
||||||
|
|
||||||
CountryNames = Rules.Info["world"].Traits.WithInterface<OpenRA.Traits.CountryInfo>().ToDictionary(a => a.Race, a => a.Name);
|
CountryNames = Rules.Info["world"].Traits.WithInterface<OpenRA.Traits.CountryInfo>()
|
||||||
|
.ToDictionary(a => a.Race, a => a.Name);
|
||||||
CountryNames.Add("random", "Random");
|
CountryNames.Add("random", "Random");
|
||||||
|
|
||||||
var mapButton = lobby.GetWidget("CHANGEMAP_BUTTON");
|
var mapButton = lobby.GetWidget("CHANGEMAP_BUTTON");
|
||||||
mapButton.OnMouseUp = mi =>
|
mapButton.OnMouseUp = mi =>
|
||||||
{
|
{
|
||||||
Widget.OpenWindow( "MAP_CHOOSER", new WidgetArgs() { { "orderManager", orderManager }, { "mapName", MapUid } } );
|
Widget.OpenWindow("MAP_CHOOSER", new WidgetArgs()
|
||||||
|
{
|
||||||
|
{ "orderManager", orderManager },
|
||||||
|
{ "mapName", MapUid }
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,11 +228,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShowSlotDropDown(DropDownButtonWidget dropdown, Session.Slot slot)
|
bool ShowSlotDropDown(DropDownButtonWidget dropdown, Session.Slot slot, Session.Client client)
|
||||||
{
|
{
|
||||||
var options = new List<SlotDropDownOption>()
|
var options = new List<SlotDropDownOption>()
|
||||||
{
|
{
|
||||||
new SlotDropDownOption("Open", "slot_open "+slot.PlayerReference, () => (!slot.Closed && slot.Bot == null)),
|
new SlotDropDownOption("Open", "slot_open "+slot.PlayerReference, () => (!slot.Closed && client == null)),
|
||||||
new SlotDropDownOption("Closed", "slot_close "+slot.PlayerReference, () => slot.Closed)
|
new SlotDropDownOption("Closed", "slot_close "+slot.PlayerReference, () => slot.Closed)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,7 +240,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
foreach (var b in Rules.Info["player"].Traits.WithInterface<IBotInfo>().Select(t => t.Name))
|
foreach (var b in Rules.Info["player"].Traits.WithInterface<IBotInfo>().Select(t => t.Name))
|
||||||
{
|
{
|
||||||
var bot = b;
|
var bot = b;
|
||||||
options.Add(new SlotDropDownOption("Bot: {0}".F(bot), "slot_bot {0} {1}".F(slot.PlayerReference, bot), () => slot.Bot == bot));
|
options.Add(new SlotDropDownOption("Bot: {0}".F(bot),
|
||||||
|
"slot_bot {0} {1}".F(slot.PlayerReference, bot),
|
||||||
|
() => client != null && client.Bot == bot));
|
||||||
}
|
}
|
||||||
|
|
||||||
Func<SlotDropDownOption, ScrollItemWidget, ScrollItemWidget> setupItem = (o, itemTemplate) =>
|
Func<SlotDropDownOption, ScrollItemWidget, ScrollItemWidget> setupItem = (o, itemTemplate) =>
|
||||||
@@ -327,32 +336,30 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
var c = orderManager.LobbyInfo.ClientInSlot(kv.Key);
|
var c = orderManager.LobbyInfo.ClientInSlot(kv.Key);
|
||||||
Widget template;
|
Widget template;
|
||||||
|
|
||||||
if (c == null)
|
if (c == null || c.Bot != null)
|
||||||
{
|
{
|
||||||
if (Game.IsHost)
|
if (Game.IsHost)
|
||||||
{
|
{
|
||||||
template = EmptySlotTemplateHost.Clone();
|
template = EmptySlotTemplateHost.Clone();
|
||||||
var name = template.GetWidget<DropDownButtonWidget>("NAME");
|
var name = template.GetWidget<DropDownButtonWidget>("NAME");
|
||||||
name.GetText = () => s.Closed ? "Closed" : (s.Bot == null) ? "Open" : s.Bot;
|
name.GetText = () => s.Closed ? "Closed" : (c == null) ? "Open" : c.Bot;
|
||||||
name.OnMouseDown = _ => ShowSlotDropDown(name, s);
|
name.OnMouseDown = _ => ShowSlotDropDown(name, s, c);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
template = EmptySlotTemplate.Clone();
|
template = EmptySlotTemplate.Clone();
|
||||||
var name = template.GetWidget<LabelWidget>("NAME");
|
var name = template.GetWidget<LabelWidget>("NAME");
|
||||||
name.GetText = () => s.Closed ? "Closed" : (s.Bot == null) ? "Open" : s.Bot;
|
name.GetText = () => s.Closed ? "Closed" : (c == null) ? "Open" : c.Bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
var join = template.GetWidget<ButtonWidget>("JOIN");
|
var join = template.GetWidget<ButtonWidget>("JOIN");
|
||||||
if (join != null)
|
if (join != null)
|
||||||
{
|
{
|
||||||
join.OnMouseUp = _ => { orderManager.IssueOrder(Order.Command("slot " + s.PlayerReference)); return true; };
|
join.OnMouseUp = _ => { orderManager.IssueOrder(Order.Command("slot " + s.PlayerReference)); return true; };
|
||||||
join.IsVisible = () => !s.Closed && s.Bot == null && orderManager.LocalClient.State != Session.ClientState.Ready;
|
join.IsVisible = () => !s.Closed && c == null && orderManager.LocalClient.State != Session.ClientState.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bot = template.GetWidget<LabelWidget>("BOT");
|
template.GetWidget<LabelWidget>("BOT").IsVisible = () => c != null;
|
||||||
if (bot != null)
|
|
||||||
bot.IsVisible = () => s.Bot != null;
|
|
||||||
}
|
}
|
||||||
else if (c.Index == orderManager.LocalClient.Index && c.State != Session.ClientState.Ready)
|
else if (c.Index == orderManager.LocalClient.Index && c.State != Session.ClientState.Ready)
|
||||||
{
|
{
|
||||||
@@ -401,11 +408,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
var status = template.GetWidget<CheckboxWidget>("STATUS");
|
||||||
status.IsChecked = () => c.State == Session.ClientState.Ready;
|
status.IsChecked = () => c.State == Session.ClientState.Ready;
|
||||||
status.OnClick = CycleReady;
|
status.OnClick = CycleReady;
|
||||||
|
|
||||||
var spectator = template.GetWidget<LabelWidget>("SPECTATOR");
|
|
||||||
|
|
||||||
Session.Slot ss = s;
|
|
||||||
spectator.IsVisible = () => ss.Bot != null;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -429,11 +431,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic
|
|||||||
if (c.Index == orderManager.LocalClient.Index)
|
if (c.Index == orderManager.LocalClient.Index)
|
||||||
status.OnClick = CycleReady;
|
status.OnClick = CycleReady;
|
||||||
|
|
||||||
var spectator = template.GetWidget<LabelWidget>("SPECTATOR");
|
|
||||||
|
|
||||||
Session.Slot ss = s;
|
|
||||||
spectator.IsVisible = () => ss.Bot != null;
|
|
||||||
|
|
||||||
var kickButton = template.GetWidget<ButtonWidget>("KICK");
|
var kickButton = template.GetWidget<ButtonWidget>("KICK");
|
||||||
kickButton.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index;
|
kickButton.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index;
|
||||||
kickButton.OnMouseUp = mi =>
|
kickButton.OnMouseUp = mi =>
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ Container@SERVER_LOBBY:
|
|||||||
Width:150
|
Width:150
|
||||||
Height:25
|
Height:25
|
||||||
MaxLength:16
|
MaxLength:16
|
||||||
|
Visible:false
|
||||||
|
DropDownButton@BOT_DROPDOWN:
|
||||||
|
Id:BOT_DROPDOWN
|
||||||
|
Text:Name
|
||||||
|
Width:150
|
||||||
|
Height:25
|
||||||
|
Font:Regular
|
||||||
|
Visible:false
|
||||||
DropDownButton@COLOR:
|
DropDownButton@COLOR:
|
||||||
Id:COLOR
|
Id:COLOR
|
||||||
Width:80
|
Width:80
|
||||||
@@ -207,38 +215,6 @@ Container@SERVER_LOBBY:
|
|||||||
Height:25
|
Height:25
|
||||||
X:160
|
X:160
|
||||||
Y:0
|
Y:0
|
||||||
Container@TEMPLATE_BOT:
|
|
||||||
Id:TEMPLATE_BOT
|
|
||||||
X:5
|
|
||||||
Y:0
|
|
||||||
Width:475
|
|
||||||
Height:25
|
|
||||||
Visible:false
|
|
||||||
Children:
|
|
||||||
DropDownButton@NAME_HOST:
|
|
||||||
Id:NAME_HOST
|
|
||||||
Text:Name
|
|
||||||
Width:150
|
|
||||||
Height:25
|
|
||||||
Font:Regular
|
|
||||||
Visible:false
|
|
||||||
Label@NAME:
|
|
||||||
Id:NAME
|
|
||||||
Text:Name
|
|
||||||
Width:145
|
|
||||||
Height:25
|
|
||||||
X:5
|
|
||||||
Y:0-1
|
|
||||||
Visible:false
|
|
||||||
Label@BOT:
|
|
||||||
Id:BOT
|
|
||||||
Text:Bot
|
|
||||||
Width:278
|
|
||||||
Height:25
|
|
||||||
X:160
|
|
||||||
Y:0
|
|
||||||
Align:Center
|
|
||||||
Font:Bold
|
|
||||||
Container@TEMPLATE_LOCAL_SPECTATOR:
|
Container@TEMPLATE_LOCAL_SPECTATOR:
|
||||||
Id:TEMPLATE_LOCAL_SPECTATOR
|
Id:TEMPLATE_LOCAL_SPECTATOR
|
||||||
X:5
|
X:5
|
||||||
|
|||||||
@@ -98,15 +98,6 @@ Background@SERVER_LOBBY:
|
|||||||
Y:2
|
Y:2
|
||||||
Width:20
|
Width:20
|
||||||
Height:20
|
Height:20
|
||||||
Label@SPECTATOR:
|
|
||||||
Id:SPECTATOR
|
|
||||||
Text:Spectator
|
|
||||||
Width:278
|
|
||||||
Height:25
|
|
||||||
X:160
|
|
||||||
Y:0
|
|
||||||
Align:Center
|
|
||||||
Font:Bold
|
|
||||||
Container@TEMPLATE_REMOTE:
|
Container@TEMPLATE_REMOTE:
|
||||||
Id:TEMPLATE_REMOTE
|
Id:TEMPLATE_REMOTE
|
||||||
X:5
|
X:5
|
||||||
@@ -170,15 +161,6 @@ Background@SERVER_LOBBY:
|
|||||||
Y:2
|
Y:2
|
||||||
Width:20
|
Width:20
|
||||||
Height:20
|
Height:20
|
||||||
Label@SPECTATOR:
|
|
||||||
Id:SPECTATOR
|
|
||||||
Text:Spectator
|
|
||||||
Width:278
|
|
||||||
Height:25
|
|
||||||
X:160
|
|
||||||
Y:0
|
|
||||||
Align:Center
|
|
||||||
Font:Bold
|
|
||||||
Container@TEMPLATE_EMPTY:
|
Container@TEMPLATE_EMPTY:
|
||||||
Id:TEMPLATE_EMPTY
|
Id:TEMPLATE_EMPTY
|
||||||
X:5
|
X:5
|
||||||
|
|||||||
Reference in New Issue
Block a user