Sieges: reinforcements log in for registered clans and log out after, walls excluded, siege state/skills set for bots; bots count as online clan members (clan skills); dashboard telemetry panel

This commit is contained in:
2026-09-03 20:57:06 +02:00
parent 97ce6ae2af
commit 5fc54be523
4 changed files with 137 additions and 8 deletions
@@ -38,6 +38,8 @@ canvas{width:100%;height:100%;display:block;background:#0d1014}
<div class="sub">живой срез мира, обновление каждые 5 сек</div>
<div class="lbl">Итого</div>
<div id="stats"></div>
<div class="lbl">Что реально сделали</div>
<div id="telemetry" class="sub"></div>
<div class="lbl">Фильтр</div>
<input id="q" placeholder="поиск по нику...">
<div style="margin-top:6px">
@@ -180,6 +182,17 @@ document.getElementById('pGo').onclick=()=>{
})};
refresh();setInterval(refresh,5000);
function telemetry(){
fetch('/api/stats').then(r=>r.json()).then(t=>{
const keys=['kills','exp','loot','itemsSold','adenaEarned','adenaSpent','stores','skillsCast','rests','lines','replies','parties','migrations','professions','sieges','pvpKills','deaths'];
let h='';
keys.forEach(k=>{h+='<div class="stat"><span>'+k+'</span><b>'+(t[k]??0)+'</b></div>';});
h+='<div class="stat"><span>сейчас</span><b>бой '+t.fighting+' · сидят '+t.sitting+' · торгуют '+t.trading+' · в пати '+t.inParty+' · на осаде '+t.besieging+'</b></div>';
h+='<div class="stat"><span>аптайм</span><b>'+t.uptimeMin+' мин</b></div>';
document.getElementById('telemetry').innerHTML=h;
}).catch(()=>{});
}
telemetry();setInterval(telemetry,10000);
window.addEventListener('resize',draw);
</script>
</body>
@@ -79,6 +79,7 @@ public class FakePlayerBotState
public boolean giftedStock = false;
// --- siege ---
public boolean temporary = false; // Logged in on demand for a siege; leaves the world afterwards.
public byte siegeSide = 0; // 0 none, 1 attacker, 2 defender.
public int siegeCastleId = 0;
public long siegeNextThink = 0;
@@ -191,7 +191,9 @@ public class FakePlayerHeadlessManager
// Believable player title instead of a personality label.
player.setTitle(TITLES[Rnd.get(TITLES.length)]);
// Restore clan membership so crests, clan wars and sieges work like for real players.
// The bot is online for the engine (clan member lists, sieges, parties count it like a logged in player).
player.setOnlineStatus(true, false);
// Restore clan membership so crests, clan skills, clan wars and sieges work like for real players.
if (player.getClanId() > 0)
{
final Clan clan = ClanTable.getInstance().getClan(player.getClanId());
@@ -200,6 +202,11 @@ public class FakePlayerHeadlessManager
player.setClan(clan);
player.setPledgeType(0);
player.setPowerGrade((clan.getLeaderId() == player.getObjectId()) ? 1 : 6);
final org.l2jmobius.gameserver.entity.clan.ClanMember member = clan.getClanMember(player.getObjectId());
if (member != null)
{
member.setPlayer(player); // Clan skills, siege skills for the leader, "online" for the clan.
}
}
}
// Place the bot in a level appropriate farming zone (shared zone table). Clanmates of a
@@ -247,6 +254,16 @@ public class FakePlayerHeadlessManager
* @return the headless player, or null.
*/
public Player ensureOnline(int charId)
{
return ensureOnline(charId, false);
}
/**
* @param charId the character id.
* @param temporary true when the bot should log out again after its job (siege reinforcement).
* @return the headless player, or null.
*/
public Player ensureOnline(int charId, boolean temporary)
{
if (charId <= 0)
{
@@ -266,11 +283,46 @@ public class FakePlayerHeadlessManager
final Player bot = spawnOne(charId);
if (bot != null)
{
LOGGER.info(getClass().getSimpleName() + ": " + bot.getName() + " logged in on demand (" + _bots.size() + " headless players now).");
final FakePlayerBotState state = stateOf(bot);
if ((state != null) && temporary)
{
state.temporary = true;
}
LOGGER.info(getClass().getSimpleName() + ": " + bot.getName() + " logged in on demand" + (temporary ? " for a siege" : "") + " (" + _bots.size() + " headless players now).");
}
return bot;
}
/**
* Logs a temporary bot out (siege reinforcements after the battle): stores it and removes it from the world.
* @param bot the bot.
*/
public void despawn(Player bot)
{
final FakePlayerBotState state = stateOf(bot);
if ((state == null) || !state.temporary || (state.siegeSide != 0))
{
return;
}
try
{
closeStore(bot);
if (bot.getParty() != null)
{
bot.leaveParty();
}
_states.remove(bot.getObjectId());
_bots.remove(bot);
bot.setOnlineStatus(false, false);
bot.deleteMe();
LOGGER.info(getClass().getSimpleName() + ": " + bot.getName() + " logged out (" + _bots.size() + " headless players now).");
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not log out " + bot.getName(), e);
}
}
// ======================== Registry ========================
/**
@@ -287,17 +287,60 @@ public class FakePlayerSiegeManager
}
_battles.put(castle.getResidenceId(), battle);
// Reinforcements: a registered clan brings its strongest members even if they were not in the
// random population sample (they log out again after the battle).
final Set<Integer> battleClans = new HashSet<>(battle.attackerClans);
battleClans.addAll(battle.defenderClans);
for (int clanId : battleClans)
{
final Clan clan = ClanTable.getInstance().getClan(clanId);
if (clan == null)
{
continue;
}
int online = 0;
for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
{
if ((bot.getClanId() == clanId) && (bot.getLevel() >= (MIN_LEVEL - 10)))
{
online++;
}
}
final List<org.l2jmobius.gameserver.entity.clan.ClanMember> strongest = new ArrayList<>(clan.getMembers());
strongest.sort((a, b) -> Integer.compare(b.getLevel(), a.getLevel()));
for (org.l2jmobius.gameserver.entity.clan.ClanMember member : strongest)
{
if ((online >= 8) || (member.getLevel() < MIN_LEVEL))
{
break;
}
if (FakePlayerHeadlessManager.getInstance().ensureOnline(member.getObjectId(), true) != null)
{
online++;
}
}
FakePlayerHeadlessManager.getInstance().ensureOnline(clan.getLeaderId(), true);
}
// Participants: online headless members of the registered clans.
final Map<Integer, Integer> perClan = new HashMap<>();
int members = 0;
int tooLow = 0;
for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
{
final int clanId = bot.getClanId();
final boolean attacker = battle.attackerClans.contains(clanId);
final boolean defender = battle.defenderClans.contains(clanId);
if ((!attacker && !defender) || (bot.getLevel() < (MIN_LEVEL - 10)))
if (!attacker && !defender)
{
continue;
}
members++;
if (bot.getLevel() < (MIN_LEVEL - 10))
{
tooLow++;
continue;
}
final int count = perClan.getOrDefault(clanId, 0);
if ((count >= MAX_PER_CLAN) && !bot.isClanLeader())
{
@@ -315,6 +358,12 @@ public class FakePlayerSiegeManager
state.siegesJoined.incrementAndGet();
state.action((attacker ? "siege attack " : "siege defend ") + castle.getName());
bot.setSiegeState(state.siegeSide);
bot.setSiegeSide(castle.getResidenceId());
if (bot.isClanLeader() && (bot.getKnownSkill(CommonSkill.SEAL_OF_RULER.getId()) == null))
{
SiegeManager.getInstance().addSiegeSkills(bot);
}
bot.updateUserInfo();
FakePlayerBotContext.setPlan(state.seedId, "siege", 3 * 3600000L);
FakePlayerBotContext.setSlot(state.seedId, "castle", castle.getName());
battle.participants.add(bot);
@@ -345,7 +394,7 @@ public class FakePlayerSiegeManager
ThreadPool.schedule(() -> cry(bot, seam), 6000 + Rnd.get(4000));
}
}
LOGGER.info(getClass().getSimpleName() + ": Siege of " + castle.getName() + " - " + battle.participants.size() + " bots joined (attacker clans " + battle.attackerClans + ", defender clans " + battle.defenderClans + ").");
LOGGER.info(getClass().getSimpleName() + ": Siege of " + castle.getName() + " - " + battle.participants.size() + " bots joined of " + members + " clan members online (" + tooLow + " too low), attacker clans " + battle.attackerClans + ", defender clans " + battle.defenderClans + ".");
}
/**
@@ -365,7 +414,7 @@ public class FakePlayerSiegeManager
double best = -1;
for (Door door : castle.getDoors())
{
if (!door.isShowHp())
if (!door.isShowHp() || door.isWall())
{
continue;
}
@@ -487,7 +536,7 @@ public class FakePlayerSiegeManager
Door gate = null;
for (Door door : battle.castle.getDoors())
{
if (door.isOpen() || door.isDead() || !door.isAutoAttackable(bot))
if (door.isOpen() || door.isDead() || door.isWall() || !door.isAutoAttackable(bot))
{
continue;
}
@@ -563,13 +612,27 @@ public class FakePlayerSiegeManager
}
state.siegeSide = 0;
state.siegeCastleId = 0;
bot.setSiegeState((byte) 0);
bot.setSiegeSide(0);
bot.setInSiege(false);
bot.updateUserInfo();
FakePlayerBotContext.setPlan(state.seedId, "farm", 0);
FakePlayerBotContext.setEvent(state.seedId, "siege_finished");
state.action("siege over");
// Back to the farming spot after a while (the engine already sends everybody to town).
// Back to the farming spot after a while (the engine already sends everybody to town);
// reinforcements that only came for the battle log out instead.
ThreadPool.schedule(() ->
{
if (!bot.isDead() && (FakePlayerHeadlessManager.getInstance().stateOf(bot) != null) && (FakePlayerHeadlessManager.getInstance().stateOf(bot).siegeSide == 0))
final FakePlayerBotState current = FakePlayerHeadlessManager.getInstance().stateOf(bot);
if ((current == null) || (current.siegeSide != 0))
{
return;
}
if (current.temporary)
{
FakePlayerHeadlessManager.getInstance().despawn(bot);
}
else if (!bot.isDead())
{
FakePlayerHeadlessManager.getInstance().returnToSpot(bot);
}