mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
mischief: a contract that never landed shouldn't protect its target
The 24h cooldown and the one-boss-a-week cap both keyed off any resolved contract, and a fizzle resolves. So a target could have a friend point a grunt at them, extract, and buy a day of immunity for the fizzle rake. Both caps now only count contracts that were actually delivered. Delivery also ran a full combat against the target's sheet without their advUserLock. hasActiveCombatSession only sees the turn engine — the target's own autopilot walk resolves its fights inline under that lock and reports no session, so a delivery could race it and lose a fight's worth of HP writes. Also: don't tell a buyer a rival beat them when the insert simply failed.
This commit is contained in:
@@ -77,32 +77,54 @@ func (p *AdventurePlugin) fireMischiefDeliveries(now time.Time) {
|
||||
p.sweepStaleMischief(now)
|
||||
for _, c := range loadMischiefDue(now) {
|
||||
contract := c
|
||||
target := contract.TargetID
|
||||
p.fireOneMischiefDelivery(&contract)
|
||||
}
|
||||
}
|
||||
|
||||
exp, err := getActiveExpedition(target)
|
||||
if err != nil || exp == nil || exp.Status != ExpeditionStatusActive {
|
||||
p.fizzleMischief(&contract)
|
||||
continue
|
||||
}
|
||||
// Same safety net the ambient ticker keeps: the expedition row can still
|
||||
// say 'active' after the player has functionally left the dungeon.
|
||||
if exp.RunID != "" {
|
||||
if run, _ := getZoneRun(exp.RunID); run == nil || !run.IsActive() {
|
||||
p.fizzleMischief(&contract)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if hasActiveCombatSession(target) {
|
||||
continue // they're mid-fight; the monster can wait a minute
|
||||
}
|
||||
if !claimMischiefForDelivery(contract.ID) {
|
||||
continue // another tick (or another process) already has it
|
||||
}
|
||||
if err := p.deliverMischief(&contract, exp); err != nil {
|
||||
slog.Error("mischief: delivery failed",
|
||||
"contract", contract.ID, "target", target, "err", err)
|
||||
// fireOneMischiefDelivery resolves a single due contract under the TARGET's lock.
|
||||
//
|
||||
// The lock is not optional. hasActiveCombatSession only sees the turn-based
|
||||
// engine; the target's own `!explore` / autopilot walk resolves its fights
|
||||
// inline (runHarvestInterrupt → runZoneCombatRoster) under advUserLock and
|
||||
// reports no session. Without taking that same lock, a delivery can run a second
|
||||
// combat against the same character sheet concurrently — two LoadDnDCharacter →
|
||||
// mutate → SaveDnDCharacter writers, last write wins, and a whole fight's HP cost
|
||||
// vanishes. The boredom ticker takes this lock for exactly this reason.
|
||||
//
|
||||
// Everything below the lock runs on the auto-resolve path, which takes no user
|
||||
// locks of its own, so there is nothing here to deadlock against.
|
||||
func (p *AdventurePlugin) fireOneMischiefDelivery(contract *mischiefContract) {
|
||||
target := contract.TargetID
|
||||
|
||||
lock := p.advUserLock(target)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
// Re-read under the lock: whatever the due-sweep saw, the expedition may have
|
||||
// ended while we were queuing behind the target's own command.
|
||||
exp, err := getActiveExpedition(target)
|
||||
if err != nil || exp == nil || exp.Status != ExpeditionStatusActive {
|
||||
p.fizzleMischief(contract)
|
||||
return
|
||||
}
|
||||
// Same safety net the ambient ticker keeps: the expedition row can still
|
||||
// say 'active' after the player has functionally left the dungeon.
|
||||
if exp.RunID != "" {
|
||||
if run, _ := getZoneRun(exp.RunID); run == nil || !run.IsActive() {
|
||||
p.fizzleMischief(contract)
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasActiveCombatSession(target) {
|
||||
return // they're mid-fight; the monster can wait a minute
|
||||
}
|
||||
if !claimMischiefForDelivery(contract.ID) {
|
||||
return // another tick (or another process) already has it
|
||||
}
|
||||
if err := p.deliverMischief(contract, exp); err != nil {
|
||||
slog.Error("mischief: delivery failed",
|
||||
"contract", contract.ID, "target", target, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sweepStaleMischief releases contracts stranded in `delivering` — the process
|
||||
@@ -378,8 +400,9 @@ func (p *AdventurePlugin) resolveMischiefDowned(
|
||||
// releases the party and would leave us nobody to floor.
|
||||
p.floorMischiefRoster(c.TargetID)
|
||||
|
||||
// forcedExtractExpedition retires the region runs and releases the party itself;
|
||||
// only the zone run has to be closed here.
|
||||
_ = abandonZoneRun(c.TargetID)
|
||||
_ = retireAllRegionRuns(exp)
|
||||
_, tax, err := forcedExtractExpedition(exp.ID, "mischief contract")
|
||||
if err != nil {
|
||||
slog.Warn("mischief: force extract failed", "expedition", exp.ID, "err", err)
|
||||
|
||||
Reference in New Issue
Block a user