@@ -146,26 +146,9 @@ private async void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventAr
146146 break ;
147147
148148 case PwPhase . DefendCollect :
149- // check if enough defenders volunteered — start 30s countdown
150- var totalSlots = FormedSquads . Sum ( s => s . TeamSize ) ;
151- var totalDefenders = DefenderVotes . Values . Sum ( v => v . Count ) ;
152- if ( totalDefenders >= totalSlots )
153- {
154- if ( defendersFullTime == null ) defendersFullTime = DateTime . UtcNow ;
155- }
156- else
157- {
158- defendersFullTime = null ;
159- }
160-
161- var deadline = GetDefendDeadline ( ) ;
162- if ( defendersFullTime != null )
163- {
164- var earlyDeadline = defendersFullTime . Value . AddSeconds ( 30 ) ;
165- if ( earlyDeadline < deadline ) deadline = earlyDeadline ;
166- }
149+ UpdateDefendersFullTime ( ) ;
167150
168- if ( DateTime . UtcNow > deadline )
151+ if ( DateTime . UtcNow > GetEffectiveDefendDeadline ( ) )
169152 {
170153 defendersFullTime = null ;
171154 RunDefenderAssignment ( ) ;
@@ -352,16 +335,34 @@ private void RunDefenderAssignment()
352335 }
353336 }
354337
355- // floating pool fills unfilled slots on other planets (WHR order)
338+ // floating pool fills unfilled slots on other planets (WHR order, respecting faction eligibility )
356339 floatingPool = floatingPool . OrderByDescending ( x => defenderWhr . Get ( x ) ) . ToList ( ) ;
340+
341+ // cache defending factions per planet and defender faction IDs
342+ var planetDefendingFactions = new Dictionary < int , List < Faction > > ( ) ;
343+ foreach ( var pid in attackedPlanetIds )
344+ planetDefendingFactions [ pid ] = GetDefendingFactions ( FormedSquads . First ( s => s . PlanetID == pid ) ) ;
345+
346+ var defenderFactionId = new Dictionary < string , int ? > ( ) ;
347+ using ( var db = new ZkDataContext ( ) )
348+ {
349+ foreach ( var name in floatingPool )
350+ {
351+ var user = server . ConnectedUsers . Get ( name ) ? . User ;
352+ if ( user != null ) defenderFactionId [ name ] = db . Accounts . Find ( user . AccountID ) ? . FactionID ;
353+ }
354+ }
355+
357356 foreach ( var planetId in attackedPlanetIds )
358357 {
359358 var totalSlotsNeeded = FormedSquads . Where ( s => s . PlanetID == planetId ) . Sum ( s => s . TeamSize ) ;
360359 var assigned = assignedDefenders [ planetId ] ;
361360 var deficit = totalSlotsNeeded - assigned . Count ;
362361 if ( deficit > 0 && floatingPool . Count > 0 )
363362 {
364- var toAdd = floatingPool . Take ( deficit ) . ToList ( ) ;
363+ var allowedFactionIds = planetDefendingFactions [ planetId ] . Select ( f => f . FactionID ) . ToHashSet ( ) ;
364+ var eligible = floatingPool . Where ( x => defenderFactionId . ContainsKey ( x ) && defenderFactionId [ x ] . HasValue && allowedFactionIds . Contains ( defenderFactionId [ x ] . Value ) ) . ToList ( ) ;
365+ var toAdd = eligible . Take ( deficit ) . ToList ( ) ;
365366 assigned . AddRange ( toAdd ) ;
366367 foreach ( var p in toAdd ) floatingPool . Remove ( p ) ;
367368 }
@@ -544,11 +545,15 @@ private async Task JoinPlanetDefense(int targetPlanetId, string userName)
544545 var account = db . Accounts . Find ( user . AccountID ) ;
545546 if ( account == null || ! account . CanPlayerPlanetWars ( ) ) return ;
546547
547- // check this user's faction can defend at least one squad on this planet
548+ // check this user's faction can defend this specific planet
548549 var squadsOnPlanet = FormedSquads . Where ( s => s . PlanetID == targetPlanetId ) . ToList ( ) ;
549550 if ( ! squadsOnPlanet . Any ( ) ) return ;
550551 var defendingFactions = GetDefendingFactions ( squadsOnPlanet . First ( ) ) ;
551- if ( ! defendingFactions . Any ( f => f . FactionID == account . FactionID ) ) return ;
552+ if ( ! defendingFactions . Any ( f => f . FactionID == account . FactionID ) )
553+ {
554+ await server . GhostChanSay ( user . Faction , $ "{ userName } cannot defend { squadsOnPlanet . First ( ) . Name } (not owner or allied)") ;
555+ return ;
556+ }
552557
553558 // remove from other planets
554559 foreach ( var kv in DefenderVotes )
@@ -558,6 +563,7 @@ private async Task JoinPlanetDefense(int targetPlanetId, string userName)
558563 if ( ! DefenderVotes [ targetPlanetId ] . Contains ( userName ) )
559564 {
560565 DefenderVotes [ targetPlanetId ] . Add ( userName ) ;
566+ UpdateDefendersFullTime ( ) ;
561567 await server . GhostChanSay ( user . Faction , $ "{ userName } joins defense of { squadsOnPlanet . First ( ) . Name } ") ;
562568 await conus . SendCommand ( new PwJoinPlanetSuccess ( ) { PlanetID = targetPlanetId } ) ;
563569 await UpdateLobby ( ) ;
@@ -600,6 +606,8 @@ public async Task OnUserDisconnected(string name)
600606 // also remove from formed squads (attacker who disconnected after squad formation)
601607 foreach ( var squad in FormedSquads )
602608 changed |= squad . Attackers . RemoveAll ( x => x == name ) > 0 ;
609+
610+ if ( changed ) UpdateDefendersFullTime ( ) ;
603611 }
604612
605613 if ( changed ) await UpdateLobby ( ) ;
@@ -613,7 +621,7 @@ public async Task OnUserDisconnected(string name)
613621
614622 // ===================== LOBBY COMMANDS =====================
615623
616- public PwMatchCommand GenerateLobbyCommand ( )
624+ public PwMatchCommand GenerateLobbyCommand ( string playerFaction = null )
617625 {
618626 PwMatchCommand command = null ;
619627 try
@@ -633,10 +641,30 @@ public PwMatchCommand GenerateLobbyCommand()
633641 }
634642 else if ( Phase == PwPhase . DefendCollect )
635643 {
636- // aggregate per planet: one VoteOption per planet showing total slots needed
644+ // build defending factions cache per planet
645+ var defFactionCache = new Dictionary < int , List < Faction > > ( ) ;
646+ foreach ( var planetId in FormedSquads . Select ( s => s . PlanetID ) . Distinct ( ) )
647+ {
648+ if ( ! defFactionCache . ContainsKey ( planetId ) )
649+ defFactionCache [ planetId ] = GetDefendingFactions ( FormedSquads . First ( s => s . PlanetID == planetId ) ) ;
650+ }
651+
652+ // resolve player's faction ID for filtering
653+ int ? playerFactionId = null ;
654+ if ( playerFaction != null )
655+ {
656+ var fac = factions . FirstOrDefault ( f => f . Shortcut == playerFaction ) ;
657+ playerFactionId = fac ? . FactionID ;
658+ }
659+
660+ // aggregate per planet, filtered to planets this player can defend
637661 var options = new List < PwMatchCommand . VoteOption > ( ) ;
638662 foreach ( var planetId in FormedSquads . Select ( s => s . PlanetID ) . Distinct ( ) )
639663 {
664+ // skip planets this player's faction cannot defend
665+ if ( playerFactionId != null && ! defFactionCache [ planetId ] . Any ( f => f . FactionID == playerFactionId ) )
666+ continue ;
667+
640668 var squads = FormedSquads . Where ( s => s . PlanetID == planetId ) . ToList ( ) ;
641669 var first = squads . First ( ) ;
642670 var totalNeeded = squads . Sum ( s => s . TeamSize ) ;
@@ -655,23 +683,17 @@ public PwMatchCommand GenerateLobbyCommand()
655683 } ) ;
656684 }
657685
658- // collect all defending factions across attacked planets (one DB call per distinct planet, not per squad)
659- var defFactionCache = new Dictionary < int , List < Faction > > ( ) ;
660- foreach ( var pid in options . Select ( o => o . PlanetID ) )
661- {
662- if ( ! defFactionCache . ContainsKey ( pid ) )
663- defFactionCache [ pid ] = GetDefendingFactions ( FormedSquads . First ( s => s . PlanetID == pid ) ) ;
664- }
665686 var allDefFactions = defFactionCache . Values
666687 . SelectMany ( f => f . Select ( x => x . Shortcut ) )
667688 . Distinct ( )
668689 . ToList ( ) ;
669690
691+ var effectiveDeadline = GetEffectiveDefendDeadline ( ) ;
670692 command = new PwMatchCommand ( PwMatchCommand . ModeType . Defend )
671693 {
672694 Options = options ,
673- Deadline = GetDefendDeadline ( ) ,
674- DeadlineSeconds = ( int ) GetDefendDeadline ( ) . Subtract ( DateTime . UtcNow ) . TotalSeconds ,
695+ Deadline = effectiveDeadline ,
696+ DeadlineSeconds = ( int ) effectiveDeadline . Subtract ( DateTime . UtcNow ) . TotalSeconds ,
675697 AttackerFaction = AttackingFaction . Shortcut ,
676698 DefenderFactions = allDefFactions
677699 } ;
@@ -834,20 +856,73 @@ private DateTime GetDefendDeadline()
834856 return PhaseStartTime . AddMinutes ( GlobalConst . PlanetWarsMinutesToAccept ) ;
835857 }
836858
859+ /// <summary>
860+ /// Effective defend deadline accounting for the 30s early cutoff when enough defenders join.
861+ /// </summary>
862+ private DateTime GetEffectiveDefendDeadline ( )
863+ {
864+ var deadline = GetDefendDeadline ( ) ;
865+ if ( defendersFullTime != null )
866+ {
867+ var earlyDeadline = defendersFullTime . Value . AddSeconds ( 30 ) ;
868+ if ( earlyDeadline < deadline ) deadline = earlyDeadline ;
869+ }
870+ return deadline ;
871+ }
872+
873+ /// <summary>
874+ /// Check if every attacked planet has enough direct defender volunteers and set/clear the 30s countdown.
875+ /// Each planet must independently have volunteers >= its slots — overflow across factions is not counted.
876+ /// </summary>
877+ private void UpdateDefendersFullTime ( )
878+ {
879+ var allFull = FormedSquads . Any ( ) ; // at least one squad exists
880+ foreach ( var planetId in FormedSquads . Select ( s => s . PlanetID ) . Distinct ( ) )
881+ {
882+ var slotsNeeded = FormedSquads . Where ( s => s . PlanetID == planetId ) . Sum ( s => s . TeamSize ) ;
883+ var volunteered = DefenderVotes . ContainsKey ( planetId ) ? DefenderVotes [ planetId ] . Count : 0 ;
884+ if ( volunteered < slotsNeeded )
885+ {
886+ allFull = false ;
887+ break ;
888+ }
889+ }
890+
891+ if ( allFull )
892+ {
893+ if ( defendersFullTime == null ) defendersFullTime = DateTime . UtcNow ;
894+ }
895+ else
896+ {
897+ defendersFullTime = null ;
898+ }
899+ }
900+
837901 public void RemoveFromRunningBattles ( int battleID )
838902 {
839903 RunningBattles . Remove ( battleID ) ;
840904 }
841905
842906 private async Task UpdateLobby ( )
843907 {
844- await server . Broadcast ( server . ConnectedUsers . Values . Where ( x => x . User . CanUserPlanetWars ( ) ) , GenerateLobbyCommand ( ) ) ;
908+ if ( Phase == PwPhase . DefendCollect )
909+ {
910+ // send per-player filtered options (only planets they can defend)
911+ foreach ( var conus in server . ConnectedUsers . Values . Where ( x => x . User . CanUserPlanetWars ( ) ) )
912+ await conus . SendCommand ( GenerateLobbyCommand ( conus . User . Faction ) ) ;
913+ }
914+ else
915+ {
916+ await server . Broadcast ( server . ConnectedUsers . Values . Where ( x => x . User . CanUserPlanetWars ( ) ) , GenerateLobbyCommand ( ) ) ;
917+ }
845918 SaveStateToDb ( ) ;
846919 }
847920
848921 private Task UpdateLobby ( string player )
849922 {
850- return server . ConnectedUsers . Get ( player ) ? . SendCommand ( GenerateLobbyCommand ( ) ) ;
923+ var conus = server . ConnectedUsers . Get ( player ) ;
924+ if ( conus == null ) return Task . CompletedTask ;
925+ return conus . SendCommand ( GenerateLobbyCommand ( conus . User . Faction ) ) ;
851926 }
852927
853928 private void SaveStateToDb ( )
0 commit comments