Skip to content

Commit 9cf8f06

Browse files
committed
Decouple client updates from server tick
- Refactored client update pipeline to separate time-based eligibility from throughput-based execution - Introduced bounded worker pool with per-client in-flight gating to prevent overlapping updates - Removed unnecessary async/await from client update path (synchronous Update() semantics) - Ensured server tick never blocks on slow or stalled clients - Range-bounded ground quality pillar updates to nearby items only
1 parent 563692b commit 9cf8f06

File tree

4 files changed

+431
-64
lines changed

4 files changed

+431
-64
lines changed
142 Bytes
Binary file not shown.

Zolian.Server.Base/GameScripts/Monsters/AdvancedMonsterAI.cs

Lines changed: 255 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Darkages.Enums;
66
using Darkages.Network.Client;
77
using Darkages.Network.Server;
8+
using Darkages.Object;
89
using Darkages.ScriptingBase;
910
using Darkages.Sprites;
1011
using Darkages.Sprites.Entity;
@@ -1619,4 +1620,257 @@ public override void OnApproach(WorldClient client)
16191620
AddObject(summoned);
16201621
}
16211622
}
1622-
}
1623+
}
1624+
1625+
[Script("ChaosHydraLava")]
1626+
public class ChaosHydraLava : MonsterScript
1627+
{
1628+
private readonly string _aosdaSayings = "Teine remembers all who burn… and all who scream.|You step into my fire. Is mise an tine.|My heads speak with one voice, bás i ngach lasair.|Stone fears me. Water flees me. Flesh feeds me.";
1629+
private string[] GhostChat => _aosdaSayings.Split(['|'], StringSplitOptions.RemoveEmptyEntries);
1630+
private int Count => GhostChat.Length;
1631+
private bool _deathCry;
1632+
private bool _phaseOne;
1633+
private bool _phaseTwo;
1634+
private bool _phaseThree;
1635+
1636+
public ChaosHydraLava(Monster monster, Area map) : base(monster, map)
1637+
{
1638+
Monster.CastTimer.RandomizedVariance = 60;
1639+
Monster.AbilityTimer.RandomizedVariance = 50;
1640+
Monster.MonsterBank = [];
1641+
}
1642+
1643+
/// <summary>
1644+
/// Overwritten to "Ao Sith" remove debuffs if poisoned or vulnerable; Adds UpdatePhases()
1645+
/// to spawn adds at 75%, 50%, and 25% health thresholds
1646+
/// </summary>
1647+
public override void Update(TimeSpan elapsedTime)
1648+
{
1649+
var monster = Monster;
1650+
if (monster is null || !monster.IsAlive)
1651+
return;
1652+
1653+
var update = monster.ObjectUpdateTimer.Update(elapsedTime);
1654+
1655+
try
1656+
{
1657+
if (update)
1658+
{
1659+
monster.ObjectUpdateEnabled = true;
1660+
monster.UpdateTarget();
1661+
UpdatePhases();
1662+
}
1663+
1664+
monster.ObjectUpdateEnabled = false;
1665+
1666+
if (monster.IsVulnerable || monster.IsPoisoned)
1667+
{
1668+
if (!monster.VulnerabilityWatch.IsRunning)
1669+
monster.VulnerabilityWatch.Start();
1670+
1671+
if (monster.VulnerabilityWatch.Elapsed.TotalMilliseconds > 3000)
1672+
{
1673+
var pos = monster.Pos;
1674+
var diceRoll = Generator.RandNumGen100();
1675+
if (diceRoll >= 80)
1676+
monster.SendAnimationNearby(75, new Position(pos));
1677+
1678+
foreach (var debuff in monster.Debuffs.Values)
1679+
debuff?.OnEnded(monster, debuff);
1680+
1681+
monster.VulnerabilityWatch.Restart();
1682+
}
1683+
}
1684+
1685+
MonsterState(elapsedTime);
1686+
}
1687+
catch (Exception e)
1688+
{
1689+
ServerSetup.EventsLogger($"{e}\nUnhandled exception in {GetType().Name}.{nameof(Update)}");
1690+
SentrySdk.CaptureException(e);
1691+
}
1692+
}
1693+
1694+
private void UpdatePhases()
1695+
{
1696+
var monster = Monster;
1697+
if (monster is null || !monster.IsAlive)
1698+
return;
1699+
1700+
if (monster.CurrentHp <= monster.MaximumHp * 0.75 && !_phaseOne)
1701+
{
1702+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Shout, $"{monster.Name}: Every age ends in fire. I am merely patient."));
1703+
monster.Image = 206; // Two-headed hydra
1704+
monster.DefenseElement = ElementManager.Element.Sorrow;
1705+
monster.UpdateAddAndRemove();
1706+
_phaseOne = true;
1707+
}
1708+
1709+
if (monster.CurrentHp <= monster.MaximumHp * 0.50 && !_phaseTwo)
1710+
{
1711+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Shout, $"{monster.Name}: The world was forged once. I am what remains."));
1712+
monster.Image = 315; // One-headed hydra
1713+
monster.OffenseElement = ElementManager.Element.Sorrow;
1714+
monster.UpdateAddAndRemove();
1715+
_phaseTwo = true;
1716+
}
1717+
1718+
if (monster.CurrentHp <= monster.MaximumHp * 0.25 && !_phaseThree)
1719+
{
1720+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Shout, $"{monster.Name}: Fire is not chaos. Fire is memory."));
1721+
monster.OffenseElement = ElementManager.Element.Rage;
1722+
monster.DefenseElement = ElementManager.Element.Rage;
1723+
_phaseThree = true;
1724+
}
1725+
}
1726+
1727+
/// <summary>
1728+
/// Overwritten to display a death message
1729+
/// </summary>
1730+
public override void OnDeath(WorldClient client = null)
1731+
{
1732+
var monster = Monster;
1733+
if (monster is null) return;
1734+
1735+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Normal, $"{monster.Name}: Remember my name when the fire takes you."));
1736+
1737+
Task.Delay(300).Wait();
1738+
1739+
var bank = monster.MonsterBank;
1740+
for (var i = 0; i < bank.Count; i++)
1741+
{
1742+
var item = bank[i];
1743+
if (item is null)
1744+
continue;
1745+
1746+
item.Release(monster, monster.Position);
1747+
AddObject(item);
1748+
1749+
foreach (var player in item.AislingsNearby())
1750+
item.ShowTo(player);
1751+
}
1752+
1753+
if (monster.Target is null)
1754+
{
1755+
Aisling found = null;
1756+
1757+
foreach (var kvp in monster.TargetRecord.TaggedAislings)
1758+
{
1759+
var p = kvp.Value;
1760+
if (p?.Map == monster.Map)
1761+
{
1762+
found = p;
1763+
break;
1764+
}
1765+
}
1766+
1767+
monster.Target = found;
1768+
}
1769+
1770+
if (monster.Target is Aisling aisling)
1771+
{
1772+
monster.GenerateRewards(aisling);
1773+
Monster.UpdateKillCounters(monster);
1774+
}
1775+
else
1776+
{
1777+
var level = monster.Template.Level;
1778+
var sum = (uint)Random.Shared.Next(level * 13, level * 200);
1779+
1780+
if (sum > 0)
1781+
Money.Create(monster, sum, new Position(monster.Pos.X, monster.Pos.Y));
1782+
}
1783+
1784+
monster.Remove();
1785+
}
1786+
1787+
/// <summary>
1788+
/// Overwritten to allow commentary while walking, casting spells, and allow to see invisible players
1789+
/// </summary>
1790+
public override void MonsterState(TimeSpan elapsedTime)
1791+
{
1792+
var monster = Monster;
1793+
if (monster is null || !monster.IsAlive)
1794+
return;
1795+
1796+
if (monster.Target is not null && monster.TargetRecord.TaggedAislings.IsEmpty &&
1797+
monster.Template.EngagedWalkingSpeed > 0)
1798+
monster.WalkTimer.Delay = TimeSpan.FromMilliseconds(monster.Template.EngagedWalkingSpeed);
1799+
else
1800+
monster.WalkTimer.Delay = TimeSpan.FromMilliseconds(monster.Template.MovementSpeed);
1801+
1802+
var assail = monster.BashTimer.Update(elapsedTime);
1803+
var ability = monster.AbilityTimer.Update(elapsedTime);
1804+
var cast = monster.CastTimer.Update(elapsedTime);
1805+
var walk = monster.WalkTimer.Update(elapsedTime);
1806+
1807+
if (monster.Target is Aisling aisling)
1808+
{
1809+
if (monster.Target.IsWeakened && !_deathCry)
1810+
{
1811+
_deathCry = true;
1812+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Normal, $"{monster.Name}: Enough. Éiríonn an tine fiáin!"));
1813+
}
1814+
1815+
if (aisling.Skulled || aisling.Dead || !aisling.LoggedIn)
1816+
{
1817+
monster.ClearTarget();
1818+
1819+
if (monster.Template.MoodType.MoodFlagIsSet(MoodQualifer.Neutral))
1820+
monster.Aggressive = false;
1821+
1822+
if (monster.CantMove || !monster.WalkEnabled) return;
1823+
if (walk) monster.PreWalkChecks();
1824+
1825+
return;
1826+
}
1827+
1828+
if (monster.BashEnabled && assail && !monster.CantAttack)
1829+
monster.Bash();
1830+
1831+
if (monster.AbilityEnabled && ability && !monster.CantAttack)
1832+
monster.Abilities();
1833+
1834+
if (monster.CastEnabled && cast && !monster.CantCast)
1835+
CastSpell();
1836+
}
1837+
1838+
if (monster.WalkEnabled && walk && !monster.CantMove)
1839+
{
1840+
monster.PreWalkChecks();
1841+
var rand = Generator.RandomPercentPrecise();
1842+
if (rand >= 0.93)
1843+
{
1844+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Normal, $"{monster.Name}: {GhostChat[RandomNumberGenerator.GetInt32(Count + 1) % GhostChat.Length]}"));
1845+
}
1846+
}
1847+
1848+
monster.UpdateTarget(false, true);
1849+
}
1850+
1851+
private void CastSpell()
1852+
{
1853+
var monster = Monster;
1854+
if (monster is null || monster.CantCast)
1855+
return;
1856+
1857+
var target = monster.Target;
1858+
if (target is null) return;
1859+
1860+
if (!target.WithinMonsterSpellRangeOf(monster)) return;
1861+
1862+
monster.SendTargetedClientMethod(PlayerScope.NearbyAislings, c => c.SendPublicMessage(monster.Serial, PublicMessageType.Normal, $"{monster.Name}: One head watches. One judges. One burns."));
1863+
1864+
var scripts = monster.SpellScripts;
1865+
1866+
// Training Dummy or other enemies who can't attack
1867+
if (scripts.Count == 0) return;
1868+
1869+
if (Generator.RandomPercentPrecise() < 0.50)
1870+
return;
1871+
1872+
var idx = RandomNumberGenerator.GetInt32(scripts.Count);
1873+
var script = scripts[idx];
1874+
script?.OnUse(monster, target);
1875+
}
1876+
}

0 commit comments

Comments
 (0)