ModernUO/Projects/UOContent/Engines/Player Murder System/BountyBoard.cs
Jack 9f39198fab
feat: Adds pre-T2A-pub15 bounty system (#2377)
# Bounty Boards

<img width="717" height="382" alt="image" src="https://github.com/user-attachments/assets/455e5206-47d8-4449-805c-19b143d059e5" />
<img width="918" height="637" alt="image" src="https://github.com/user-attachments/assets/e78e1ca2-62b8-4abf-8f69-21438bb1b759" />
<img width="340" height="296" alt="image" src="https://github.com/user-attachments/assets/fd17d7e6-8c53-47df-ac58-a89ea3ef665e" />

## Setup
* Setup as part of decorate when bounty system is enabled
  * Several bounty board locations with a WarriorGuard spawner in front of the board. Guard spawns and idles around 5 range.

## Tests

### ReportBountyMurdererGump
* Follows same behaviour as ReportMurdererGump
* Extracted common logic
* Didn't use staticgump due to several dynamic parts including input
* Optional bounty with validation >0 and <bankbox.total
* Murder report is honored either way

### Bounty boards
* Open bounty board with many bounties, no bounties
* Keep a bounty board open, invalidate a bounty by turning in the head, then try to click on the post of the now invalid post. As expected: does nothing
* Bounty messages use the last murder time as their post date and expire in 14 days
* Bounty boards/messages are not reliant on serialization; they use serialized fields from MurderContext to build messages when clicked.
* Bounty messages use synthetic serials so they do not have to persist bounty messages as items. They are constructed and sent as raw packets when needed.
* Players only appear on the bounty board if they are a murderer (though a non-murderer can technically still have a bounty if they decayed kills)
* Tested skin/hair color descriptions vs a dozen spot checks

### Head turn in behaviour
* Guard accepts head
  * Bounty -> gives bounty
  * No bounty -> generic response
  * Expired head (24h) -> generic response

NOTE: CUO "latest" has a bug with bounty/bulletinmessages that causes overflow outside of the container. It has nothing to do with this PR. It is fixed here in CUO https://github.com/ClassicUO/ClassicUO/pull/1871

# Murderer title

Bounty system and "murderer" title eliminated in [pub16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/). So UO:LBR and before had bounties and murderer title.

# Pre-T2A caveat

There were differences in pre-T2A, but this cannot be currently implemented because we do not have any pre-T2A systems in general, so at least for now, behavior is consistent with later eras. Pre-T2A was a whole other ballgame, but the bounty system still worked similarly.
2026-03-22 12:35:16 -07:00

90 lines
3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Buffers;
using ModernUO.Serialization;
using Server.Engines.PlayerMurderSystem;
using Server.Mobiles;
using Server.Network;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class BountyBoard : BaseBulletinBoard
{
// Offset added to player serials (max 0x3FFFFFFF) to produce synthetic serials
// in the unused range (0x800000010xBFFFFFFF) for BB packets. Reversible by subtraction.
private const uint SyntheticSerialBase = 0x80000000;
[Constructible]
public BountyBoard() : base(0x1E5E) => BoardName = "bounty board";
public override void OnSingleClick(Mobile from)
{
var count = PlayerMurderSystem.GetActiveBountyCount();
if (count > 0)
{
LabelTo(from, $"a bounty board with {count} posted {(count == 1 ? "bounty" : "bounties")}");
}
else
{
LabelTo(from, 1042679); // a bounty board with no bounties posted.
}
}
public override void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines)
{
from.SendLocalizedMessage(1062398); // You are not allowed to post to this bulletin board.
}
public override void OnDoubleClick(Mobile from)
{
if (!CheckRange(from))
{
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that.
return;
}
var state = from.NetState;
state.SendBBDisplayBoard(this);
BountyMessage.SendBountyContainerContent(state, this, SyntheticSerialBase);
}
public override bool HandleBBRequest(int packetID, Mobile from, SpanReader reader)
{
switch (packetID)
{
case 3: // Request content
case 4: // Request header
{
var syntheticSerial = reader.ReadUInt32();
var playerSerial = (Serial)(syntheticSerial - SyntheticSerialBase);
if (World.FindMobile(playerSerial) is PlayerMobile player &&
PlayerMurderSystem.GetMurderContext(player, out var context) &&
context.Bounty > 0)
{
BountyMessage.SendBountyBBMessage(
from.NetState,
this,
(Serial)syntheticSerial,
player,
context.Bounty,
context.LastMurderTime,
packetID == 3
);
}
return true;
}
case 5: // Post - blocked
{
from.SendLocalizedMessage(1062398); // You are not allowed to post to this bulletin board.
return true;
}
case 6: // Remove - blocked
{
return true;
}
}
return false;
}
}