diff --git a/.idea/.idea.Zero-K/.idea/projectSettingsUpdater.xml b/.idea/.idea.Zero-K/.idea/projectSettingsUpdater.xml
index 64af657f5..ef20cb08c 100644
--- a/.idea/.idea.Zero-K/.idea/projectSettingsUpdater.xml
+++ b/.idea/.idea.Zero-K/.idea/projectSettingsUpdater.xml
@@ -2,6 +2,7 @@
+
\ No newline at end of file
diff --git a/Fixer/Program.cs b/Fixer/Program.cs
index 70ab686d4..a76099d18 100644
--- a/Fixer/Program.cs
+++ b/Fixer/Program.cs
@@ -431,17 +431,22 @@ private static void TestPwMatchMaker()
{
var server = new global::ZkLobbyServer.ZkLobbyServer("", new PlanetwarsEventCreator());
var mm = server.PlanetWarsMatchMaker;
- mm.ChallengeTime = DateTime.Now;
mm.AttackerSideCounter = 1;
- //mm.ResetAttackOptions();
- mm.GenerateLobbyCommand();
- mm.Challenge = mm.AttackOptions[3];
- mm.Challenge.OwnerFactionID = 2;
- mm.Challenge.PlanetID = 4375;
- mm.GetDefendingFactions(mm.Challenge);
mm.GenerateLobbyCommand();
- //mm.Challenge =
+ // simulate defend phase with a formed squad
+ if (mm.AttackOptions.Count > 3)
+ {
+ var opt = mm.AttackOptions[3];
+ opt.OwnerFactionID = 2;
+ opt.PlanetID = 4375;
+ mm.FormedSquads.Add(opt);
+ mm.Phase = PwPhase.DefendCollect;
+ mm.PhaseStartTime = DateTime.UtcNow;
+ mm.GetDefendingFactions(opt);
+ mm.GenerateLobbyCommand();
+ }
+
Global.Server.PlanetWarsMatchMaker.GenerateLobbyCommand();
}
diff --git a/Shared/PlasmaShared/GlobalConst.cs b/Shared/PlasmaShared/GlobalConst.cs
index ae1e4dbf5..aec6c31ff 100644
--- a/Shared/PlasmaShared/GlobalConst.cs
+++ b/Shared/PlasmaShared/GlobalConst.cs
@@ -210,8 +210,8 @@ static void SetMode(ModeType newMode)
public const int PostVoteHideThreshold = -6;
public const bool OnlyAdminsSeePostVoters = false;
public const int PlanetWarsMinutesToAttackIfNoOption = 2;
- public const int PlanetWarsMinutesToAttack = 20;
- public const int PlanetWarsMinutesToAccept = 5;
+ public const int PlanetWarsMinutesToAttack = 5;
+ public const int PlanetWarsMinutesToAccept = 10;
public const int PlanetWarsDropshipsStayForMinutes = 2*60;
public const int PlanetWarsMaxTeamsize = 4;
public const double PlanetWarsDefenderWinKillCcMultiplier = 0.2;
diff --git a/Shared/PlasmaShared/Utils.cs b/Shared/PlasmaShared/Utils.cs
index 249f1dc81..24baf9a70 100644
--- a/Shared/PlasmaShared/Utils.cs
+++ b/Shared/PlasmaShared/Utils.cs
@@ -537,12 +537,18 @@ public static Thread SafeThread(Action action)
public static U Get(this IDictionary dict, T key)
- where U : class
{
U val = default(U);
if (key != null) dict.TryGetValue(key, out val);
return val;
}
+
+ public static U GetOrDefault(this IDictionary dict, T key, U defaultValue)
+ {
+ U val = defaultValue;
+ if (key != null) dict.TryGetValue(key, out val);
+ return val;
+ }
public static List Shuffle(this IEnumerable source)
diff --git a/ZkLobbyServer/SpringieInterface/BattleResultHandler.cs b/ZkLobbyServer/SpringieInterface/BattleResultHandler.cs
index 6ec45d631..3523a01eb 100644
--- a/ZkLobbyServer/SpringieInterface/BattleResultHandler.cs
+++ b/ZkLobbyServer/SpringieInterface/BattleResultHandler.cs
@@ -184,7 +184,7 @@ private static void ProcessPlanetWars(SpringBattleContext result, ZkLobbyServer.
if (winNum > 1) winNum = null;
}
- PlanetWarsTurnHandler.EndTurn(result.LobbyStartContext.Map,
+ PlanetWarsTurnHandler.ProcessBattleResult(result.LobbyStartContext.Map,
result.OutputExtras,
db,
winNum,
diff --git a/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMaker.cs b/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMaker.cs
index dae6ac55a..e52f9e045 100644
--- a/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMaker.cs
+++ b/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMaker.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -8,6 +8,7 @@
using LobbyClient;
using Newtonsoft.Json;
using PlasmaShared;
+using Ratings;
using ZkData;
using ZkLobbyServer;
@@ -20,13 +21,12 @@ public class PlanetWarsMatchMaker : PlanetWarsMatchMakerState
{
private readonly List factions;
-
private int missedDefenseCount = 0;
private int missedDefenseFactionID = 0;
private ZkLobbyServer.ZkLobbyServer server;
-
private Timer timer;
+
///
/// Faction that should attack this turn
///
@@ -35,18 +35,16 @@ public class PlanetWarsMatchMaker : PlanetWarsMatchMakerState
public PlanetWarsMatchMaker(ZkLobbyServer.ZkLobbyServer server)
{
-
this.server = server;
AttackOptions = new List();
+ FormedSquads = new List();
+ DefenderVotes = new Dictionary>();
RunningBattles = new Dictionary();
-
var db = new ZkDataContext();
-
var gal = db.Galaxies.FirstOrDefault(x => x.IsDefault);
if (gal == null) return;
-
factions = db.Factions.Where(x => !x.IsDeleted).ToList();
PlanetWarsMatchMakerState dbState = null;
@@ -62,16 +60,20 @@ public PlanetWarsMatchMaker(ZkLobbyServer.ZkLobbyServer server)
if (dbState != null)
{
AttackerSideCounter = dbState.AttackerSideCounter;
- AttackOptions = dbState.AttackOptions;
- Challenge = dbState.Challenge;
- ChallengeTime = dbState.ChallengeTime;
+ AttackOptions = dbState.AttackOptions ?? new List();
+ Phase = dbState.Phase;
+ PhaseStartTime = dbState.PhaseStartTime;
+ FormedSquads = dbState.FormedSquads ?? new List();
+ DefenderVotes = dbState.DefenderVotes ?? new Dictionary>();
AttackerSideChangeTime = dbState.AttackerSideChangeTime;
- RunningBattles = dbState.RunningBattles;
+ RunningBattles = dbState.RunningBattles ?? new Dictionary();
}
else
{
AttackerSideCounter = gal.AttackerSideCounter;
AttackerSideChangeTime = gal.AttackerSideChangeTime ?? DateTime.UtcNow;
+ Phase = PwPhase.AttackCollect;
+ PhaseStartTime = GetNextTurnBoundary();
}
timer = new Timer(1045);
@@ -80,339 +82,643 @@ public PlanetWarsMatchMaker(ZkLobbyServer.ZkLobbyServer server)
timer.Start();
}
- private async Task AcceptChallenge()
+
+ // ===================== TIMER / STATE MACHINE =====================
+
+ private PlanetWarsModes? lastPlanetWarsMode;
+
+ private async void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
{
- if (missedDefenseFactionID == Challenge.OwnerFactionID)
+ try
{
- missedDefenseCount = 0;
- missedDefenseFactionID = 0;
- }
+ timer.Stop();
- // only really start if attackers are present, otherwise missed battle opportunity basically
- Challenge.Attackers = Challenge.Attackers.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
- Challenge.Defenders = Challenge.Defenders.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
- if (Challenge.Attackers.Any() || Challenge.Defenders.Any())
- {
- var battle = new PlanetWarsServerBattle(server, Challenge);
- await server.AddBattle(battle);
- RunningBattles[battle.BattleID] = Challenge;
+ // auto change PW mode based on time
+ if (MiscVar.PlanetWarsNextModeTime != null && MiscVar.PlanetWarsNextModeTime < DateTime.UtcNow && MiscVar.PlanetWarsNextMode != null)
+ {
+ MiscVar.PlanetWarsMode = MiscVar.PlanetWarsNextMode ?? PlanetWarsModes.AllOffline;
+ MiscVar.PlanetWarsNextMode = null;
+ MiscVar.PlanetWarsNextModeTime = null;
- // also join in lobby
- foreach (var usr in Challenge.Attackers.Union(Challenge.Defenders)) await server.ForceJoinBattle(usr, battle);
+ using (var db = new ZkDataContext())
+ {
+ db.Events.Add(server.PlanetWarsEventCreator.CreateEvent("PlanetWars changed status to {0}", MiscVar.PlanetWarsMode.Description()));
+ db.SaveChanges();
+ }
+ }
- if (await battle.StartGame())
+ if (MiscVar.PlanetWarsMode != lastPlanetWarsMode)
{
+ server.Broadcast(GeneratePwStatus());
+ UpdateLobby();
+ lastPlanetWarsMode = MiscVar.PlanetWarsMode;
+ }
- var text =
- $"Battle for planet {Challenge.Name} starts on zk://@join_player:{Challenge.Attackers.FirstOrDefault()} Roster: {string.Join(",", Challenge.Attackers)} vs {string.Join(",", Challenge.Defenders)}";
+ if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return;
+
+ // clean up stale running battles (e.g. if Spring process crashed)
+ var staleBattleIds = RunningBattles.Keys.Where(id => !server.Battles.ContainsKey(id)).ToList();
+ foreach (var id in staleBattleIds) RunningBattles.Remove(id);
+
+ switch (Phase)
+ {
+ case PwPhase.AttackCollect:
+ if (DateTime.UtcNow > GetAttackDeadline())
+ {
+ RunSquadFormation();
+ if (FormedSquads.Any())
+ {
+ // transition to defend
+ Phase = PwPhase.DefendCollect;
+ PhaseStartTime = DateTime.UtcNow;
+ UpdateLobby();
+ }
+ else
+ {
+ // nobody attacked, skip to next faction
+ AttackerSideCounter++;
+ ResetAttackOptions();
+ }
+ }
+ break;
- foreach (var fac in factions) await server.GhostChanSay(fac.Shortcut, text);
+ case PwPhase.DefendCollect:
+ if (DateTime.UtcNow > GetDefendDeadline())
+ {
+ RunDefenderAssignment();
+ await LaunchAllBattles();
+ RunGalaxyTick();
+ AttackerSideCounter++;
+ ResetAttackOptions();
+ }
+ break;
}
- else await server.RemoveBattle(battle);
}
-
- AttackerSideCounter++;
- ResetAttackOptions();
+ catch (Exception ex)
+ {
+ Trace.TraceError("PlanetWars timer error: {0}", ex);
+ }
+ finally
+ {
+ timer.Start();
+ }
}
- ///
- /// Invoked from web page
- ///
- ///
- public void AddAttackOption(Planet planet)
+
+ // ===================== SQUAD FORMATION (PIERCING) =====================
+
+ private void RunSquadFormation()
{
- try
+ FormedSquads.Clear();
+
+ // collect all attackers still connected, grouped by planet
+ var playerPlanet = new Dictionary(); // player -> their chosen option
+ foreach (var opt in AttackOptions)
{
- if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return;
+ opt.Attackers = opt.Attackers.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
+ foreach (var name in opt.Attackers)
+ playerPlanet[name] = opt;
+ }
- if (!AttackOptions.Any(x => x.PlanetID == planet.PlanetID) && (Challenge == null) &&
- (planet.OwnerFactionID != AttackingFaction.FactionID))
+ if (!playerPlanet.Any()) return;
+
+ // look up PW-WHR and PW-Rank for each player
+ var playerWhr = new Dictionary();
+ var playerRoleOrder = new Dictionary(); // lower = higher faction rank
+ using (var db = new ZkDataContext())
+ {
+ foreach (var name in playerPlanet.Keys.ToList())
{
- InternalAddOption(planet);
- UpdateLobby();
+ var user = server.ConnectedUsers.Get(name)?.User;
+ if (user == null) { playerPlanet.Remove(name); continue; }
+
+ playerWhr[name] = GetPlayerWhr(name);
+
+ // PW-Rank: faction role DisplayOrder, lower = higher rank. No role = int.MaxValue
+ var account = db.Accounts.Find(user.AccountID);
+ var factionRole = account?.AccountRolesByAccountID
+ .Where(r => r.RoleType != null && !r.RoleType.IsClanOnly && r.RoleType.RestrictFactionID == AttackingFaction.FactionID)
+ .Select(r => r.RoleType.DisplayOrder)
+ .OrderBy(x => x)
+ .Cast()
+ .FirstOrDefault();
+ playerRoleOrder[name] = factionRole ?? int.MaxValue;
}
}
- catch (Exception ex)
+
+ var pool = new HashSet(playerPlanet.Keys);
+
+ // Pass 1: while any planet has >= TeamSize players, form squads from top WHR
+ bool formed;
+ do
{
- Trace.TraceError("PlanetWars error adding option {0}: {1}", planet, ex);
+ formed = false;
+ foreach (var opt in AttackOptions)
+ {
+ var available = opt.Attackers.Where(pool.Contains).OrderByDescending(x => playerWhr.Get(x)).ToList();
+ while (available.Count >= opt.TeamSize)
+ {
+ var squad = CreateSquadFromOption(opt);
+ squad.Attackers = available.Take(opt.TeamSize).ToList();
+ FormedSquads.Add(squad);
+ foreach (var p in squad.Attackers) pool.Remove(p);
+ available = available.Skip(opt.TeamSize).ToList();
+ formed = true;
+ }
+ }
+ } while (formed); // repeat in case removing players from one planet frees up nothing, but be safe
+
+ // Pass 2: piercing — top PW-Rank player pulls others to their planet
+ while (pool.Count > 0)
+ {
+ // find top PW-Rank player (lowest DisplayOrder, tiebreak by WHR desc)
+ var leader = pool
+ .OrderBy(x => playerRoleOrder.GetOrDefault(x, int.MaxValue))
+ .ThenByDescending(x => playerWhr.Get(x))
+ .First();
+
+ var leaderOption = playerPlanet[leader];
+ if (pool.Count < leaderOption.TeamSize)
+ break; // not enough players for any squad
+
+ var fillers = pool
+ .Where(x => x != leader)
+ .OrderByDescending(x => playerWhr.Get(x))
+ .Take(leaderOption.TeamSize - 1)
+ .ToList();
+
+ if (fillers.Count < leaderOption.TeamSize - 1)
+ break; // not enough
+
+ var squad = CreateSquadFromOption(leaderOption);
+ squad.Attackers = new List { leader };
+ squad.Attackers.AddRange(fillers);
+ FormedSquads.Add(squad);
+
+ pool.Remove(leader);
+ foreach (var p in fillers) pool.Remove(p);
}
+
+ AttackOptions.Clear();
+
+ // initialize defender votes for attacked planets
+ DefenderVotes.Clear();
+ foreach (var planetId in FormedSquads.Select(s => s.PlanetID).Distinct())
+ DefenderVotes[planetId] = new List();
+
+ // notify attackers
+ foreach (var squad in FormedSquads)
+ server.Broadcast(squad.Attackers, new PwAttackingPlanet() { PlanetID = squad.PlanetID });
}
- public PwMatchCommand GenerateLobbyCommand()
+ private AttackOption CreateSquadFromOption(AttackOption source)
{
- PwMatchCommand command = null;
- try
+ return new AttackOption
{
- if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return new PwMatchCommand(PwMatchCommand.ModeType.Clear);
+ PlanetID = source.PlanetID,
+ Map = source.Map,
+ Name = source.Name,
+ OwnerFactionID = source.OwnerFactionID,
+ TeamSize = source.TeamSize,
+ PlanetImage = source.PlanetImage,
+ IconSize = source.IconSize,
+ StructureImages = source.StructureImages,
+ Attackers = new List(),
+ Defenders = new List()
+ };
+ }
- if (Challenge == null)
- command = new PwMatchCommand(PwMatchCommand.ModeType.Attack)
- {
- Options = AttackOptions.Select(x => x.ToVoteOption(PwMatchCommand.ModeType.Attack)).ToList(),
- Deadline = GetAttackDeadline(),
- DeadlineSeconds = (int)GetAttackDeadline().Subtract(DateTime.UtcNow).TotalSeconds,
- AttackerFaction = AttackingFaction.Shortcut
- };
- else
- command = new PwMatchCommand(PwMatchCommand.ModeType.Defend)
- {
- Options = new List { Challenge.ToVoteOption(PwMatchCommand.ModeType.Defend) },
- Deadline = GetAcceptDeadline(),
- DeadlineSeconds = (int)GetAcceptDeadline().Subtract(DateTime.UtcNow).TotalSeconds,
- AttackerFaction = AttackingFaction.Shortcut,
- DefenderFactions = GetDefendingFactions(Challenge).Select(x => x.Shortcut).ToList()
- };
- }
- catch (Exception ex)
+
+ // ===================== DEFENDER ASSIGNMENT =====================
+
+ private void RunDefenderAssignment()
+ {
+ // look up defender WHR
+ var defenderWhr = new Dictionary();
+ foreach (var kv in DefenderVotes)
{
- Trace.TraceError("PlanetWars {0}: {1}", nameof(GenerateLobbyCommand), ex);
+ foreach (var name in kv.Value)
+ {
+ if (defenderWhr.ContainsKey(name)) continue;
+ if (!server.ConnectedUsers.ContainsKey(name)) continue;
+ defenderWhr[name] = GetPlayerWhr(name);
+ }
}
- return command;
- }
- private async Task JoinPlanet(string name, int planetId)
- {
- try
+ // per-planet: assign defenders, overflow to pool
+ var floatingPool = new List();
+ var assignedDefenders = new Dictionary>(); // planetID -> assigned defender names
+ var attackedPlanetIds = FormedSquads.Select(s => s.PlanetID).Distinct().ToList();
+
+ foreach (var planetId in attackedPlanetIds)
{
- var user = server.ConnectedUsers.Get(name)?.User;
- if (user != null)
+ var totalSlotsNeeded = FormedSquads.Where(s => s.PlanetID == planetId).Sum(s => s.TeamSize);
+ var volunteers = (DefenderVotes.ContainsKey(planetId) ? DefenderVotes[planetId] : new List())
+ .Where(x => server.ConnectedUsers.ContainsKey(x) && defenderWhr.ContainsKey(x))
+ .OrderByDescending(x => defenderWhr[x])
+ .ToList();
+
+ if (volunteers.Count > totalSlotsNeeded)
+ {
+ assignedDefenders[planetId] = volunteers.Take(totalSlotsNeeded).ToList();
+ floatingPool.AddRange(volunteers.Skip(totalSlotsNeeded));
+ }
+ else
{
- var faction = factions.First(x => x.Shortcut == user.Faction);
- if (faction == AttackingFaction) await JoinPlanetAttack(planetId, name);
- else if ((Challenge != null) && GetDefendingFactions(Challenge).Any(y=>y.FactionID == faction.FactionID)) await JoinPlanetDefense(planetId, name);
+ assignedDefenders[planetId] = volunteers;
}
}
- catch (Exception ex)
+
+ // floating pool fills unfilled slots on other planets (WHR order)
+ floatingPool = floatingPool.OrderByDescending(x => defenderWhr.Get(x)).ToList();
+ foreach (var planetId in attackedPlanetIds)
{
- Trace.TraceError("PlanetWars {0} {1} {2} : {3}", nameof(JoinPlanet), name, planetId, ex);
+ var totalSlotsNeeded = FormedSquads.Where(s => s.PlanetID == planetId).Sum(s => s.TeamSize);
+ var assigned = assignedDefenders[planetId];
+ var deficit = totalSlotsNeeded - assigned.Count;
+ if (deficit > 0 && floatingPool.Count > 0)
+ {
+ var toAdd = floatingPool.Take(deficit).ToList();
+ assigned.AddRange(toAdd);
+ foreach (var p in toAdd) floatingPool.Remove(p);
+ }
}
- }
- public async Task OnJoinPlanet(ConnectedUser conus, PwJoinPlanet args)
- {
- if (MiscVar.PlanetWarsMode == PlanetWarsModes.Running)
+ // slice defenders into squads: sort squads by avg attacker WHR desc, assign best defenders to best attackers
+ foreach (var planetId in attackedPlanetIds)
{
- if (conus.User.CanUserPlanetWars()) await JoinPlanet(conus.Name, args.PlanetID);
+ var squadsForPlanet = FormedSquads
+ .Where(s => s.PlanetID == planetId)
+ .OrderByDescending(s => s.Attackers.Average(a => GetPlayerWhr(a))) // sort by attacker strength
+ .ToList();
+
+ var defenders = assignedDefenders.ContainsKey(planetId)
+ ? assignedDefenders[planetId].OrderByDescending(x => defenderWhr.Get(x)).ToList()
+ : new List();
+
+ int idx = 0;
+ foreach (var squad in squadsForPlanet)
+ {
+ var count = Math.Min(squad.TeamSize, defenders.Count - idx);
+ if (count == squad.TeamSize)
+ {
+ squad.Defenders = defenders.Skip(idx).Take(count).ToList();
+ idx += count;
+ }
+ // else: squad gets no defenders (concede) - Defenders stays empty
+ }
}
}
- public async Task OnLoginAccepted(ConnectedUser connectedUser)
+ private double GetPlayerWhr(string name)
{
- await connectedUser.SendCommand(GeneratePwStatus());
-
- if (MiscVar.PlanetWarsMode == PlanetWarsModes.Running)
- {
- var u = connectedUser.User;
- if (u.CanUserPlanetWars()) await UpdateLobby(u.Name);
- }
+ var user = server.ConnectedUsers.Get(name)?.User;
+ if (user == null) return 0;
+ return RatingSystems.GetRatingSystem(RatingCategory.Planetwars).GetPlayerRating(user.AccountID).LadderElo;
}
- public async Task OnUserDisconnected(string name)
+
+ // ===================== LAUNCH BATTLES =====================
+
+ private async Task LaunchAllBattles()
{
- try
+ foreach (var squad in FormedSquads)
{
- if (MiscVar.PlanetWarsMode == PlanetWarsModes.Running)
+ // filter to still-connected
+ squad.Attackers = squad.Attackers.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
+ squad.Defenders = squad.Defenders.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
+
+ if (squad.Defenders.Count == squad.TeamSize && squad.Attackers.Count == squad.TeamSize)
{
- if (Challenge == null)
+ // full battle
+ try
{
- if (AttackOptions.Count > 0)
+ var battle = new PlanetWarsServerBattle(server, squad);
+ await server.AddBattle(battle);
+ RunningBattles[battle.BattleID] = squad;
+
+ foreach (var usr in squad.Attackers.Union(squad.Defenders))
+ await server.ForceJoinBattle(usr, battle);
+
+ if (await battle.StartGame())
{
- var sumRemoved = 0;
- foreach (var aop in AttackOptions) sumRemoved += aop.Attackers.RemoveAll(x => x == name);
- if (sumRemoved > 0) await UpdateLobby();
+ var text = $"Battle for planet {squad.Name} starts on zk://@join_player:{squad.Attackers.FirstOrDefault()} Roster: {string.Join(",", squad.Attackers)} vs {string.Join(",", squad.Defenders)}";
+ foreach (var fac in factions) await server.GhostChanSay(fac.Shortcut, text);
+ }
+ else
+ {
+ await server.RemoveBattle(battle);
+ RunningBattles.Remove(battle.BattleID);
}
}
- else
+ catch (Exception ex)
{
- var userName = name;
- if (Challenge.Defenders.RemoveAll(x => x == userName) > 0) await UpdateLobby();
+ Trace.TraceError("PlanetWars LaunchBattle error: {0}", ex);
}
}
+ else if (squad.Attackers.Count == squad.TeamSize)
+ {
+ // concede - not enough defenders
+ RecordPlanetwarsLoss(squad);
+ }
+ // else: attackers also disconnected, skip entirely
+ }
+
+ FormedSquads.Clear();
+ DefenderVotes.Clear();
+ }
+
+
+ // ===================== GALAXY TICK =====================
+
+ private void RunGalaxyTick()
+ {
+ try
+ {
+ var text = new StringBuilder();
+ using (var db = new ZkDataContext())
+ {
+ PlanetWarsTurnHandler.ProcessGalaxyTick(db, text, server.PlanetWarsEventCreator, server);
+ }
}
catch (Exception ex)
{
- Trace.TraceError("PlanetWars OnUserDisconnected: {0}", ex);
+ Trace.TraceError("PlanetWars galaxy tick error: {0}", ex);
}
}
- public void RemoveFromRunningBattles(int battleID)
- {
- RunningBattles.Remove(battleID);
- }
+ // ===================== PLAYER ACTIONS =====================
- private async Task UpdateLobby()
+ public async Task OnJoinPlanet(ConnectedUser conus, PwJoinPlanet args)
{
- await
- server.Broadcast(server.ConnectedUsers.Values.Where(x => x.User.CanUserPlanetWars()), GenerateLobbyCommand());
- SaveStateToDb();
+ if (MiscVar.PlanetWarsMode == PlanetWarsModes.Running)
+ {
+ if (conus.User.CanUserPlanetWars()) await JoinPlanet(conus.Name, args.PlanetID);
+ }
}
- private Task UpdateLobby(string player)
+ private async Task JoinPlanet(string name, int planetId)
{
- return server.ConnectedUsers.Get(player)?.SendCommand(GenerateLobbyCommand());
- }
+ try
+ {
+ var user = server.ConnectedUsers.Get(name)?.User;
+ if (user == null) return;
- private DateTime GetAcceptDeadline()
- {
- return ChallengeTime.Value.AddMinutes(GlobalConst.PlanetWarsMinutesToAccept);
+ var faction = factions.FirstOrDefault(x => x.Shortcut == user.Faction);
+ if (faction == null) return;
+
+ if (Phase == PwPhase.AttackCollect && faction == AttackingFaction)
+ await JoinPlanetAttack(planetId, name);
+ else if (Phase == PwPhase.DefendCollect && faction != AttackingFaction)
+ await JoinPlanetDefense(planetId, name);
+ }
+ catch (Exception ex)
+ {
+ Trace.TraceError("PlanetWars {0} {1} {2} : {3}", nameof(JoinPlanet), name, planetId, ex);
+ }
}
- private DateTime GetAttackDeadline()
+ private async Task JoinPlanetAttack(int targetPlanetId, string userName)
{
- var extra = 0;
- if (AttackOptions.Count == 0)
+ var attackOption = AttackOptions.Find(x => x.PlanetID == targetPlanetId);
+ if (attackOption == null) return;
+
+ var conus = server.ConnectedUsers.Get(userName);
+ var user = conus?.User;
+ if (user == null) return;
+
+ using (var db = new ZkDataContext())
{
- return AttackerSideChangeTime.AddMinutes(GlobalConst.PlanetWarsMinutesToAttackIfNoOption);
- }
+ var account = db.Accounts.Find(user.AccountID);
+ if (account == null || account.FactionID != AttackingFaction.FactionID || !account.CanPlayerPlanetWars()) return;
- if (missedDefenseFactionID == AttackingFaction.FactionID) extra = Math.Min(missedDefenseCount * GlobalConst.PlanetWarsMinutesToAttack, 60);
+ // remove from other options
+ foreach (var aop in AttackOptions.Where(x => x.PlanetID != targetPlanetId))
+ aop.Attackers.RemoveAll(x => x == userName);
- return AttackerSideChangeTime.AddMinutes(GlobalConst.PlanetWarsMinutesToAttack + extra);
+ // add to this option (no cap — it's a vote, squad formation handles sizing)
+ if (!attackOption.Attackers.Contains(userName))
+ {
+ attackOption.Attackers.Add(user.Name);
+ await server.GhostChanSay(user.Faction, $"{userName} joins attack on {attackOption.Name}");
+ await conus.SendCommand(new PwJoinPlanetSuccess() { PlanetID = targetPlanetId });
+ await UpdateLobby();
+ }
+ }
}
- public List GetDefendingFactions(AttackOption target)
+ private async Task JoinPlanetDefense(int targetPlanetId, string userName)
{
- if (target.OwnerFactionID != null)
+ if (!DefenderVotes.ContainsKey(targetPlanetId)) return;
+
+ var conus = server.ConnectedUsers.Get(userName);
+ var user = conus?.User;
+ if (user == null) return;
+
+ using (var db = new ZkDataContext())
{
- var ret = new List();
- ret.Add(factions.Find(x => x.FactionID == target.OwnerFactionID));
+ var account = db.Accounts.Find(user.AccountID);
+ if (account == null || !account.CanPlayerPlanetWars()) return;
- // add allies as defenders
- using (var db = new ZkDataContext())
+ // check this user's faction can defend at least one squad on this planet
+ var squadsOnPlanet = FormedSquads.Where(s => s.PlanetID == targetPlanetId).ToList();
+ if (!squadsOnPlanet.Any()) return;
+ var defendingFactions = GetDefendingFactions(squadsOnPlanet.First());
+ if (!defendingFactions.Any(f => f.FactionID == account.FactionID)) return;
+
+ // remove from other planets
+ foreach (var kv in DefenderVotes)
+ kv.Value.RemoveAll(x => x == userName);
+
+ // add to this planet
+ if (!DefenderVotes[targetPlanetId].Contains(userName))
{
- var planet = db.Planets.Find(target.PlanetID);
- foreach (var of in db.Factions.Where(x=>!x.IsDeleted && x.FactionID != target.OwnerFactionID && x.FactionID != AttackingFaction.FactionID))
- {
- if (of.GaveTreatyRight(planet, x=>x.EffectBalanceSameSide == true)) ret.Add(factions.First(x=>x.FactionID == of.FactionID));
- }
+ DefenderVotes[targetPlanetId].Add(userName);
+ await server.GhostChanSay(user.Faction, $"{userName} joins defense of {squadsOnPlanet.First().Name}");
+ await conus.SendCommand(new PwJoinPlanetSuccess() { PlanetID = targetPlanetId });
+ await UpdateLobby();
}
- return ret;
}
-
- return factions.Where(x => x != AttackingFaction).ToList();
}
- private void InternalAddOption(Planet planet)
+
+ // ===================== CONNECTION EVENTS =====================
+
+ public async Task OnLoginAccepted(ConnectedUser connectedUser)
{
- AttackOptions.Add(new AttackOption
+ await connectedUser.SendCommand(GeneratePwStatus());
+
+ if (MiscVar.PlanetWarsMode == PlanetWarsModes.Running)
{
- PlanetID = planet.PlanetID,
- Map = planet.Resource.InternalName,
- OwnerFactionID = planet.OwnerFactionID,
- Name = planet.Name,
- TeamSize = planet.TeamSize,
- PlanetImage = planet.Resource?.MapPlanetWarsIcon,
- IconSize = planet.Resource?.PlanetWarsIconSize ?? 0,
- StructureImages = planet.PlanetStructures.Select(x => x.IsActive ? x.StructureType.MapIcon : x.StructureType.DisabledMapIcon).ToList()
- });
+ var u = connectedUser.User;
+ if (u.CanUserPlanetWars()) await UpdateLobby(u.Name);
+ }
}
- private async Task JoinPlanetAttack(int targetPlanetId, string userName)
+ public async Task OnUserDisconnected(string name)
{
- var attackOption = AttackOptions.Find(x => x.PlanetID == targetPlanetId);
- if (attackOption != null)
+ try
{
- var conus = server.ConnectedUsers.Get(userName);
- var user = conus?.User;
- if (user != null)
- using (var db = new ZkDataContext())
- {
- var account = db.Accounts.Find(user.AccountID);
- if ((account != null) && (account.FactionID == AttackingFaction.FactionID) && account.CanPlayerPlanetWars())
- {
- // remove existing user from other options
- foreach (var aop in AttackOptions.Where(x => x.PlanetID != targetPlanetId)) aop.Attackers.RemoveAll(x => x == userName);
-
- // add user to this option
- if (attackOption.Attackers.Count < attackOption.TeamSize && !attackOption.Attackers.Contains(userName))
- {
- attackOption.Attackers.Add(user.Name);
- await server.GhostChanSay(user.Faction, $"{userName} joins attack on {attackOption.Name}");
+ if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return;
- await conus.SendCommand(new PwJoinPlanetSuccess() { PlanetID = targetPlanetId });
+ bool changed = false;
+ if (Phase == PwPhase.AttackCollect)
+ {
+ foreach (var aop in AttackOptions)
+ changed |= aop.Attackers.RemoveAll(x => x == name) > 0;
+ }
+ else if (Phase == PwPhase.DefendCollect)
+ {
+ // remove from defender votes
+ foreach (var kv in DefenderVotes)
+ changed |= kv.Value.RemoveAll(x => x == name) > 0;
- if (attackOption.Attackers.Count >= attackOption.TeamSize) await StartChallenge(attackOption);
- else await UpdateLobby();
- }
- }
+ // also remove from formed squads (attacker who disconnected after squad formation)
+ foreach (var squad in FormedSquads)
+ changed |= squad.Attackers.RemoveAll(x => x == name) > 0;
+ }
- }
+ if (changed) await UpdateLobby();
+ }
+ catch (Exception ex)
+ {
+ Trace.TraceError("PlanetWars OnUserDisconnected: {0}", ex);
}
}
- private async Task JoinPlanetDefense(int targetPlanetID, string userName)
+
+ // ===================== LOBBY COMMANDS =====================
+
+ public PwMatchCommand GenerateLobbyCommand()
{
- if ((Challenge != null) && (Challenge.PlanetID == targetPlanetID) && (Challenge.Defenders.Count < Challenge.TeamSize))
+ PwMatchCommand command = null;
+ try
{
- var conus = server.ConnectedUsers.Get(userName);
- var user = conus?.User;
- if (user != null)
+ if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running)
+ return new PwMatchCommand(PwMatchCommand.ModeType.Clear);
+
+ if (Phase == PwPhase.AttackCollect)
{
- var db = new ZkDataContext();
- var account = db.Accounts.Find(user.AccountID);
- if ((account != null) && GetDefendingFactions(Challenge).Any(y => y.FactionID == account.FactionID) &&
- account.CanPlayerPlanetWars())
- if (!Challenge.Defenders.Any(y => y == user.Name))
- {
- Challenge.Defenders.Add(user.Name);
+ command = new PwMatchCommand(PwMatchCommand.ModeType.Attack)
+ {
+ Options = AttackOptions.Select(x => x.ToVoteOption(PwMatchCommand.ModeType.Attack)).ToList(),
+ Deadline = GetAttackDeadline(),
+ DeadlineSeconds = (int)GetAttackDeadline().Subtract(DateTime.UtcNow).TotalSeconds,
+ AttackerFaction = AttackingFaction.Shortcut
+ };
+ }
+ else if (Phase == PwPhase.DefendCollect)
+ {
+ // aggregate per planet: one VoteOption per planet showing total slots needed
+ var options = new List();
+ foreach (var planetId in FormedSquads.Select(s => s.PlanetID).Distinct())
+ {
+ var squads = FormedSquads.Where(s => s.PlanetID == planetId).ToList();
+ var first = squads.First();
+ var totalNeeded = squads.Sum(s => s.TeamSize);
+ var volunteered = DefenderVotes.ContainsKey(planetId) ? DefenderVotes[planetId].Count : 0;
- await server.GhostChanSay(user.Faction, $"{userName} joins defense of {Challenge.Name}");
+ options.Add(new PwMatchCommand.VoteOption
+ {
+ PlanetID = first.PlanetID,
+ PlanetName = first.Name,
+ Map = first.Map,
+ IconSize = first.IconSize,
+ StructureImages = first.StructureImages,
+ PlanetImage = first.PlanetImage,
+ Count = volunteered,
+ Needed = totalNeeded
+ });
+ }
- await conus.SendCommand(new PwJoinPlanetSuccess() { PlanetID = targetPlanetID });
+ // collect all defending factions across attacked planets (one DB call per distinct planet, not per squad)
+ var defFactionCache = new Dictionary>();
+ foreach (var pid in options.Select(o => o.PlanetID))
+ {
+ if (!defFactionCache.ContainsKey(pid))
+ defFactionCache[pid] = GetDefendingFactions(FormedSquads.First(s => s.PlanetID == pid));
+ }
+ var allDefFactions = defFactionCache.Values
+ .SelectMany(f => f.Select(x => x.Shortcut))
+ .Distinct()
+ .ToList();
- if (Challenge.Defenders.Count >= Challenge.TeamSize) await AcceptChallenge();
- else await UpdateLobby();
- }
+ command = new PwMatchCommand(PwMatchCommand.ModeType.Defend)
+ {
+ Options = options,
+ Deadline = GetDefendDeadline(),
+ DeadlineSeconds = (int)GetDefendDeadline().Subtract(DateTime.UtcNow).TotalSeconds,
+ AttackerFaction = AttackingFaction.Shortcut,
+ DefenderFactions = allDefFactions
+ };
}
}
+ catch (Exception ex)
+ {
+ Trace.TraceError("PlanetWars {0}: {1}", nameof(GenerateLobbyCommand), ex);
+ }
+ return command;
}
- private void RecordPlanetwarsLoss(AttackOption option)
- {
- if (option.OwnerFactionID != null)
- if (option.OwnerFactionID == missedDefenseFactionID)
- {
- missedDefenseCount++;
- }
- else
- {
- missedDefenseCount = 0;
- missedDefenseFactionID = option.OwnerFactionID.Value;
- }
- var message = string.Format("{0} won because nobody tried to defend", AttackingFaction.Name);
- foreach (var fac in factions) server.GhostChanSay(fac.Shortcut, message);
+ // ===================== ATTACK OPTIONS =====================
- var text = new StringBuilder();
+ ///
+ /// Invoked from web page
+ ///
+ public void AddAttackOption(Planet planet)
+ {
try
{
- var db = new ZkDataContext();
- var playerIds = option.Attackers.Select(x => x).Union(option.Defenders.Select(x => x)).ToList();
+ if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return;
+ if (Phase != PwPhase.AttackCollect) return;
- PlanetWarsTurnHandler.EndTurn(option.Map,
- null,
- db,
- 0,
- db.Accounts.Where(x => playerIds.Contains(x.Name) && (x.Faction != null)).ToList(),
- text,
- null,
- db.Accounts.Where(x => option.Attackers.Contains(x.Name) && (x.Faction != null)).ToList(),
- server.PlanetWarsEventCreator, server);
+ if (!AttackOptions.Any(x => x.PlanetID == planet.PlanetID) &&
+ (planet.OwnerFactionID != AttackingFaction.FactionID))
+ {
+ InternalAddOption(planet);
+ UpdateLobby();
+ }
}
catch (Exception ex)
{
- Trace.TraceError(ex.ToString());
- text.Append(ex);
+ Trace.TraceError("PlanetWars error adding option {0}: {1}", planet, ex);
}
}
+ ///
+ /// Total turn duration in minutes (attack + defend). Turns snap to this boundary on the wall clock.
+ ///
+ private static readonly int TurnIntervalMinutes = GlobalConst.PlanetWarsMinutesToAttack + GlobalConst.PlanetWarsMinutesToAccept;
+
+ ///
+ /// Returns the current or next wall-clock turn boundary (e.g. :00, :15, :30, :45 for 15-min turns).
+ /// If exactly on a boundary, returns that time. Otherwise returns the next one.
+ ///
+ private static DateTime GetNextTurnBoundary()
+ {
+ var now = DateTime.UtcNow;
+ // truncate to whole minutes to avoid floating point issues
+ var nowTrunc = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc);
+ var midnight = nowTrunc.Date;
+ var totalMinutes = (int)(nowTrunc - midnight).TotalMinutes;
+ var remainder = totalMinutes % TurnIntervalMinutes;
+ if (remainder == 0)
+ return nowTrunc; // already on a boundary
+ return nowTrunc.AddMinutes(TurnIntervalMinutes - remainder);
+ }
+
private void ResetAttackOptions()
{
AttackOptions.Clear();
- AttackerSideChangeTime = DateTime.UtcNow;
- Challenge = null;
- ChallengeTime = null;
+ FormedSquads.Clear();
+ DefenderVotes.Clear();
+ Phase = PwPhase.AttackCollect;
+ PhaseStartTime = GetNextTurnBoundary();
+ AttackerSideChangeTime = PhaseStartTime;
+
+ var contestedPlanetIds = RunningBattles.Values.Select(x => x.PlanetID).ToHashSet();
using (var db = new ZkDataContext())
{
@@ -424,13 +730,11 @@ private void ResetAttackOptions()
.OrderByDescending(x => x.PlanetFactions.Where(y => y.FactionID == AttackingFaction.FactionID).Sum(y => y.Dropships))
.ThenByDescending(x => x.PlanetFactions.Where(y => y.FactionID == AttackingFaction.FactionID).Sum(y => y.Influence))
.ToList();
- // list of planets by attacker's influence
foreach (var planet in planets)
{
- if (planet.CanMatchMakerPlay(attacker))
+ if (planet.CanMatchMakerPlay(attacker) && !contestedPlanetIds.Contains(planet.PlanetID))
{
- // pick only those where you can actually attack atm
InternalAddOption(planet);
cnt--;
}
@@ -439,100 +743,134 @@ private void ResetAttackOptions()
if (!AttackOptions.Any(y => y.TeamSize == 2))
{
- var planet = planets.FirstOrDefault(x => (x.TeamSize == 2) && x.CanMatchMakerPlay(attacker));
+ var planet = planets.FirstOrDefault(x => (x.TeamSize == 2) && x.CanMatchMakerPlay(attacker) && !contestedPlanetIds.Contains(x.PlanetID));
if (planet != null) InternalAddOption(planet);
}
}
UpdateLobby();
-
server.GhostChanSay(AttackingFaction.Shortcut, "It's your turn! Select a planet to attack");
}
- private void SaveStateToDb()
+ private void InternalAddOption(Planet planet)
{
- var db = new ZkDataContext();
- var gal = db.Galaxies.First(x => x.IsDefault);
-
- gal.MatchMakerState = JsonConvert.SerializeObject((PlanetWarsMatchMakerState)this);
-
- gal.AttackerSideCounter = AttackerSideCounter;
- gal.AttackerSideChangeTime = AttackerSideChangeTime;
- db.SaveChanges();
+ AttackOptions.Add(new AttackOption
+ {
+ PlanetID = planet.PlanetID,
+ Map = planet.Resource.InternalName,
+ OwnerFactionID = planet.OwnerFactionID,
+ Name = planet.Name,
+ TeamSize = planet.TeamSize,
+ PlanetImage = planet.Resource?.MapPlanetWarsIcon,
+ IconSize = planet.Resource?.PlanetWarsIconSize ?? 0,
+ StructureImages = planet.PlanetStructures.Select(x => x.IsActive ? x.StructureType.MapIcon : x.StructureType.DisabledMapIcon).ToList()
+ });
}
- private async Task StartChallenge(AttackOption attackOption)
- {
- Challenge = attackOption;
- ChallengeTime = DateTime.UtcNow;
- AttackOptions.Clear();
- await UpdateLobby();
- await server.Broadcast(attackOption.Attackers, new PwAttackingPlanet() { PlanetID = attackOption.PlanetID });
- }
+ // ===================== HELPERS =====================
- private PlanetWarsModes? lastPlanetWarsMode;
-
- private void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
+ public List GetDefendingFactions(AttackOption target)
{
- try
+ if (target.OwnerFactionID != null)
{
- timer.Stop();
+ var ret = new List();
+ ret.Add(factions.Find(x => x.FactionID == target.OwnerFactionID));
- // auto change PW mode based on time
- if (MiscVar.PlanetWarsNextModeTime != null && MiscVar.PlanetWarsNextModeTime < DateTime.UtcNow && MiscVar.PlanetWarsNextMode != null)
+ using (var db = new ZkDataContext())
{
- MiscVar.PlanetWarsMode = MiscVar.PlanetWarsNextMode ?? PlanetWarsModes.AllOffline;
-
- MiscVar.PlanetWarsNextMode = null;
- MiscVar.PlanetWarsNextModeTime = null;
-
- using (var db = new ZkDataContext())
+ var planet = db.Planets.Find(target.PlanetID);
+ foreach (var of in db.Factions.Where(x => !x.IsDeleted && x.FactionID != target.OwnerFactionID && x.FactionID != AttackingFaction.FactionID))
{
- db.Events.Add(server.PlanetWarsEventCreator.CreateEvent("PlanetWars changed status to {0}", MiscVar.PlanetWarsMode.Description()));
- db.SaveChanges();
+ if (of.GaveTreatyRight(planet, x => x.EffectBalanceSameSide == true))
+ ret.Add(factions.First(x => x.FactionID == of.FactionID));
}
}
+ return ret;
+ }
+ return factions.Where(x => x != AttackingFaction).ToList();
+ }
- if (MiscVar.PlanetWarsMode != lastPlanetWarsMode)
+ private void RecordPlanetwarsLoss(AttackOption option)
+ {
+ if (option.OwnerFactionID != null)
+ if (option.OwnerFactionID == missedDefenseFactionID)
+ missedDefenseCount++;
+ else
{
- server.Broadcast(GeneratePwStatus());
- UpdateLobby();
- lastPlanetWarsMode = MiscVar.PlanetWarsMode;
+ missedDefenseCount = 0;
+ missedDefenseFactionID = option.OwnerFactionID.Value;
}
- if (MiscVar.PlanetWarsMode != PlanetWarsModes.Running) return;
+ var message = $"{AttackingFaction.Name} won {option.Name} because nobody tried to defend";
+ foreach (var fac in factions) server.GhostChanSay(fac.Shortcut, message);
- if (Challenge == null)
- {
- // attack timer
- if (DateTime.UtcNow > GetAttackDeadline())
- {
- AttackerSideCounter++;
- ResetAttackOptions();
- }
- }
- else
+ try
+ {
+ using (var db = new ZkDataContext())
{
- // accept timer
- if (DateTime.UtcNow > GetAcceptDeadline())
- if ((Challenge.Defenders.Count >= Challenge.Attackers.Count - 1) && (Challenge.Defenders.Count > 0)) AcceptChallenge();
- else
- {
- RecordPlanetwarsLoss(Challenge);
- AttackerSideCounter++;
- ResetAttackOptions();
- }
+ var playerIds = option.Attackers.Union(option.Defenders).ToList();
+
+ PlanetWarsTurnHandler.ProcessBattleResult(option.Map,
+ null,
+ db,
+ 0,
+ db.Accounts.Where(x => playerIds.Contains(x.Name) && (x.Faction != null)).ToList(),
+ new StringBuilder(),
+ null,
+ db.Accounts.Where(x => option.Attackers.Contains(x.Name) && (x.Faction != null)).ToList(),
+ server.PlanetWarsEventCreator, server);
}
}
catch (Exception ex)
{
- Trace.TraceError("PlanetWars timer error: {0}", ex);
+ Trace.TraceError("PlanetWars RecordLoss error: {0}", ex);
}
- finally
+ }
+
+ private DateTime GetAttackDeadline()
+ {
+ if (AttackOptions.Count == 0)
+ return PhaseStartTime.AddMinutes(GlobalConst.PlanetWarsMinutesToAttackIfNoOption);
+
+ var extra = 0;
+ if (missedDefenseFactionID == AttackingFaction.FactionID)
+ extra = Math.Min(missedDefenseCount * GlobalConst.PlanetWarsMinutesToAttack, 60);
+
+ return PhaseStartTime.AddMinutes(GlobalConst.PlanetWarsMinutesToAttack + extra);
+ }
+
+ private DateTime GetDefendDeadline()
+ {
+ return PhaseStartTime.AddMinutes(GlobalConst.PlanetWarsMinutesToAccept);
+ }
+
+ public void RemoveFromRunningBattles(int battleID)
+ {
+ RunningBattles.Remove(battleID);
+ }
+
+ private async Task UpdateLobby()
+ {
+ await server.Broadcast(server.ConnectedUsers.Values.Where(x => x.User.CanUserPlanetWars()), GenerateLobbyCommand());
+ SaveStateToDb();
+ }
+
+ private Task UpdateLobby(string player)
+ {
+ return server.ConnectedUsers.Get(player)?.SendCommand(GenerateLobbyCommand());
+ }
+
+ private void SaveStateToDb()
+ {
+ using (var db = new ZkDataContext())
{
- timer.Start();
+ var gal = db.Galaxies.First(x => x.IsDefault);
+ gal.MatchMakerState = JsonConvert.SerializeObject((PlanetWarsMatchMakerState)this);
+ gal.AttackerSideCounter = AttackerSideCounter;
+ gal.AttackerSideChangeTime = AttackerSideChangeTime;
+ db.SaveChanges();
}
}
@@ -547,10 +885,12 @@ private static PwStatus GeneratePwStatus()
};
}
+
+ // ===================== NESTED TYPES =====================
+
public class AttackOption
{
public List Attackers { get; set; }
-
public List Defenders { get; set; }
public string Map { get; set; }
public string Name { get; set; }
@@ -559,8 +899,6 @@ public class AttackOption
public int TeamSize { get; set; }
public List StructureImages { get; set; } = new List();
public int IconSize { get; set; }
-
-
public string PlanetImage { get; set; }
public AttackOption()
@@ -571,7 +909,7 @@ public AttackOption()
public PwMatchCommand.VoteOption ToVoteOption(PwMatchCommand.ModeType mode)
{
- var opt = new PwMatchCommand.VoteOption
+ return new PwMatchCommand.VoteOption
{
PlanetID = PlanetID,
PlanetName = Name,
@@ -582,10 +920,7 @@ public PwMatchCommand.VoteOption ToVoteOption(PwMatchCommand.ModeType mode)
Count = mode == PwMatchCommand.ModeType.Attack ? Attackers.Count : Defenders.Count,
Needed = TeamSize
};
-
- return opt;
}
-
}
}
-}
\ No newline at end of file
+}
diff --git a/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMakerState.cs b/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMakerState.cs
index 20eea4a59..908500af2 100644
--- a/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMakerState.cs
+++ b/ZkLobbyServer/SpringieInterface/PlanetWarsMatchMakerState.cs
@@ -1,22 +1,38 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
namespace ZeroKWeb
{
+ public enum PwPhase
+ {
+ AttackCollect = 0,
+ DefendCollect = 1
+ }
+
public class PlanetWarsMatchMakerState
{
///
- /// Possible attack options
+ /// Possible attack options / planets to vote on
///
public List AttackOptions { get; set; }
public DateTime AttackerSideChangeTime { get; set; }
public int AttackerSideCounter { get; set; }
- public PlanetWarsMatchMaker.AttackOption Challenge { get; set; }
- public DateTime? ChallengeTime { get; set; }
+ public PwPhase Phase { get; set; }
+ public DateTime PhaseStartTime { get; set; }
+
+ ///
+ /// Formed attack squads after squad formation runs. Each is an AttackOption with Attackers filled.
+ ///
+ public List FormedSquads { get; set; } = new List();
+
+ ///
+ /// Defender volunteers per planet during DefendCollect phase. Key = PlanetID, Value = list of player names.
+ ///
+ public Dictionary> DefenderVotes { get; set; } = new Dictionary>();
public Dictionary RunningBattles { get; set; } = new Dictionary();
public PlanetWarsMatchMakerState() { }
}
-}
\ No newline at end of file
+}
diff --git a/ZkLobbyServer/SpringieInterface/PlanetWarsTurnHandler.cs b/ZkLobbyServer/SpringieInterface/PlanetWarsTurnHandler.cs
index b24adcd1e..03572f3b4 100644
--- a/ZkLobbyServer/SpringieInterface/PlanetWarsTurnHandler.cs
+++ b/ZkLobbyServer/SpringieInterface/PlanetWarsTurnHandler.cs
@@ -12,16 +12,18 @@ public static class PlanetWarsTurnHandler
{
///
- /// Process planet wars turn
+ /// Process planet wars turn - legacy wrapper calling both per-battle and per-turn processing
///
- ///
- ///
- ///
- /// 0 = attacker wins, 1 = defender wins
- ///
- ///
- ///
public static void EndTurn(string mapName, List extraData, ZkDataContext db, int? winNum, List players, StringBuilder text, SpringBattle sb, List attackers, IPlanetwarsEventCreator eventCreator, ZkLobbyServer.ZkLobbyServer server)
+ {
+ ProcessBattleResult(mapName, extraData, db, winNum, players, text, sb, attackers, eventCreator, server);
+ ProcessGalaxyTick(db, text, eventCreator, server, mapName, sb);
+ }
+
+ ///
+ /// Per-battle processing: influence, metal, structures, attack points
+ ///
+ public static void ProcessBattleResult(string mapName, List extraData, ZkDataContext db, int? winNum, List players, StringBuilder text, SpringBattle sb, List attackers, IPlanetwarsEventCreator eventCreator, ZkLobbyServer.ZkLobbyServer server)
{
if (extraData == null) extraData = new List();
Galaxy gal = db.Galaxies.Single(x => x.IsDefault);
@@ -57,8 +59,6 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
string influenceReport = "";
// distribute influence
- // save influence gains
- // give influence to main attackers
double planetIpDefs = planet.GetEffectiveIpDefense();
double baseInfluence = GlobalConst.BaseInfluencePerBattle;
@@ -100,7 +100,7 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
if (influence < 0) influence = 0;
influence = Math.Floor(influence * 100) / 100;
- // main winner influence
+ // main winner influence
PlanetFaction entry = planet.PlanetFactions.FirstOrDefault(x => x.Faction == attacker);
if (entry == null)
{
@@ -112,7 +112,6 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
// clamping of influence
- // gained over 100, sole owner
if (entry.Influence >= GlobalConst.PlanetWarsMaximumIP)
{
entry.Influence = GlobalConst.PlanetWarsMaximumIP;
@@ -145,8 +144,6 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
}
-
-
// distribute metal
var attackersTotalMetal = CalculateFactionMetalGain(planet, hqStructure, attacker, GlobalConst.PlanetWarsAttackerMetal, eventCreator, db, text, sb);
var attackerMetal = Math.Floor(attackersTotalMetal / attackers.Count);
@@ -222,7 +219,7 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
acc.PwAttackPoints += ap;
}
- // paranoia!
+ // event logging
try
{
var mainEvent = eventCreator.CreateEvent("{0} attacked {1} {2} in {3} and {4}. {5}",
@@ -301,13 +298,37 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
else planet.PlanetStructures.Remove(s);
}
- var ev = eventCreator.CreateEvent("All non-evacuated structures have been disabled on {0} planet {1}. {2}", planet.Faction, planet, sb);
- db.Events.InsertOnSubmit(ev);
- text.AppendLine(ev.PlainText);
+ var ev2 = eventCreator.CreateEvent("All non-evacuated structures have been disabled on {0} planet {1}. {2}", planet.Faction, planet, sb);
+ db.Events.InsertOnSubmit(ev2);
+ text.AppendLine(ev2.PlainText);
}
db.SaveChanges();
+ // planet ownership can change from battle influence — must check immediately
+ int? oldOwner = planet.OwnerAccountID;
+ SetPlanetOwners(eventCreator, db, sb);
+
+ // re-fetch planet after ownership update
+ planet = db.Galaxies.Single(x => x.IsDefault).Planets.Single(x => x.Resource.InternalName == mapName);
+ if (planet.OwnerAccountID != oldOwner && planet.OwnerAccountID != null)
+ {
+ text.AppendFormat("Congratulations!! Planet {0} was conquered by {1} !! {3}/PlanetWars/Planet/{2}\n",
+ planet.Name,
+ planet.Account.Name,
+ planet.PlanetID,
+ GlobalConst.BaseSiteUrl);
+ }
+ }
+
+ ///
+ /// Per-turn galaxy-wide processing: influence spread, decay, production, VP, treaties
+ /// Called once per turn from the matchmaker after launching battles.
+ ///
+ public static void ProcessGalaxyTick(ZkDataContext db, StringBuilder text, IPlanetwarsEventCreator eventCreator, ZkLobbyServer.ZkLobbyServer server, string mapName = null, SpringBattle sb = null)
+ {
+ Galaxy gal = db.Galaxies.Single(x => x.IsDefault);
+
gal.DecayInfluence();
gal.SpreadInfluence();
@@ -361,11 +382,10 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
foreach (var fac in db.Factions.Where(x => !x.IsDeleted)) fac.ConvertExcessEnergyToMetal();
- int? oldOwner = planet.OwnerAccountID;
gal.Turn++;
db.SaveChanges();
- db = new ZkDataContext(); // is this needed - attempt to fix setplanetownersbeing buggy
+ db = new ZkDataContext(); // attempt to fix setplanetowners being buggy
SetPlanetOwners(eventCreator, db, sb != null ? db.SpringBattles.Find(sb.SpringBattleID) : null);
gal = db.Galaxies.Single(x => x.IsDefault);
@@ -379,22 +399,10 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
db.SaveChanges();
}
-
- planet = gal.Planets.Single(x => x.Resource.InternalName == mapName);
- if (planet.OwnerAccountID != oldOwner && planet.OwnerAccountID != null)
- {
- text.AppendFormat("Congratulations!! Planet {0} was conquered by {1} !! {3}/PlanetWars/Planet/{2}\n",
- planet.Name,
- planet.Account.Name,
- planet.PlanetID,
- GlobalConst.BaseSiteUrl);
- }
-
server.PublishUserProfilePlanetwarsPlayers();
-
+
try
{
-
// store history
foreach (Planet p in gal.Planets)
{
@@ -418,27 +426,30 @@ public static void EndTurn(string mapName, List extraData, ZkDataContext
}
//rotate map
- if (GlobalConst.RotatePWMaps)
+ if (GlobalConst.RotatePWMaps && mapName != null)
{
db = new ZkDataContext();
gal = db.Galaxies.Single(x => x.IsDefault);
- planet = gal.Planets.Single(x => x.Resource.InternalName == mapName);
- var mapList = db.Resources.Where(x => x.MapPlanetWarsIcon != null && x.Planets.Where(p => p.GalaxyID == gal.GalaxyID).Count() == 0 && x.MapSupportLevel>=MapSupportLevel.Featured
- && x.ResourceID != planet.MapResourceID && x.MapWaterLevel == planet.Resource.MapWaterLevel).ToList();
- if (mapList.Count > 0)
- {
- int r = new Random().Next(mapList.Count);
- int resourceID = mapList[r].ResourceID;
- Resource newMap = db.Resources.Single(x => x.ResourceID == resourceID);
- text.AppendLine(String.Format("Map cycler - {0} maps found, selected map {1} to replace map {2}", mapList.Count, newMap.InternalName, planet.Resource.InternalName));
- planet.Resource = newMap;
- gal.IsDirty = true;
- }
- else
+ var planet2 = gal.Planets.SingleOrDefault(x => x.Resource.InternalName == mapName);
+ if (planet2 != null)
{
- text.AppendLine("Map cycler - no maps found");
+ var mapList = db.Resources.Where(x => x.MapPlanetWarsIcon != null && x.Planets.Where(p => p.GalaxyID == gal.GalaxyID).Count() == 0 && x.MapSupportLevel>=MapSupportLevel.Featured
+ && x.ResourceID != planet2.MapResourceID && x.MapWaterLevel == planet2.Resource.MapWaterLevel).ToList();
+ if (mapList.Count > 0)
+ {
+ int r = new Random().Next(mapList.Count);
+ int resourceID = mapList[r].ResourceID;
+ Resource newMap = db.Resources.Single(x => x.ResourceID == resourceID);
+ text.AppendLine(String.Format("Map cycler - {0} maps found, selected map {1} to replace map {2}", mapList.Count, newMap.InternalName, planet2.Resource.InternalName));
+ planet2.Resource = newMap;
+ gal.IsDirty = true;
+ }
+ else
+ {
+ text.AppendLine("Map cycler - no maps found");
+ }
+ db.SaveChanges();
}
- db.SaveChanges();
}
}
@@ -458,7 +469,7 @@ private static double CalculateFactionMetalGain(Planet planet, StructureType hq,
var ev = eventCreator.CreateEvent("{0} metal gain reduced by {1} because it is disconnected from {2}. {3}",
forFaction,
hq.EffectDisconnectedMetalMalus,
- hq.Name,
+ hq.Name,
sb);
db.Events.Add(ev);
texts.AppendLine(ev.PlainText);
@@ -507,8 +518,6 @@ private static List GetEvacuatedStructureTypes(List extraData, ZkDa
///
/// Updates shadow influence and new owners
///
- ///
- /// optional spring batle that caused this change (for event logging)
public static void SetPlanetOwners(IPlanetwarsEventCreator eventCreator, ZkDataContext db = null, SpringBattle sb = null)
{
if (db == null) db = new ZkDataContext();
@@ -529,7 +538,7 @@ public static void SetPlanetOwners(IPlanetwarsEventCreator eventCreator, ZkDataC
if (best == null || best.Influence < GlobalConst.InfluenceToCapturePlanet)
{
- // planet not capture
+ // planet not capture
if (planet.Faction != null)
{
@@ -572,7 +581,7 @@ public static void SetPlanetOwners(IPlanetwarsEventCreator eventCreator, ZkDataC
FirstOrDefault();
}
- // best with planets
+ // best with planets
if (candidate == null)
{
candidate =
@@ -587,7 +596,7 @@ public static void SetPlanetOwners(IPlanetwarsEventCreator eventCreator, ZkDataC
// change has occured
if (newFaction != planet.Faction)
{
- // disable structures
+ // disable structures
foreach (PlanetStructure structure in planet.PlanetStructures.Where(x => x.StructureType.OwnerChangeDisablesThis))
{
structure.ReactivateAfterBuild();