diff --git a/Distribution/Data/Decoration/BountyBoards/bountyboards.cfg b/Distribution/Data/Decoration/BountyBoards/bountyboards.cfg
new file mode 100644
index 000000000..32fb378eb
--- /dev/null
+++ b/Distribution/Data/Decoration/BountyBoards/bountyboards.cfg
@@ -0,0 +1,25 @@
+# Bounty Boards — placed when murderSystem.bountiesEnabled is true
+# 0x1E5E = east-facing, 0x1E5F = south-facing
+
+BountyBoard 0x1E5E
+2887 3482 17
+2734 2184 0
+2891 680 0
+5668 3123 13
+
+BountyBoard 0x1E5F
+5278 3981 37
+1442 1687 0
+2512 551 0
+600 2147 0
+
+# Guards — one per board, standing in front
+Spawner 0x1F13 (Spawn=WarriorGuard; Count=1; HomeRange=0; WalkingRange=5; Name=Bounty Board Guard Spawner)
+2887 3487 17
+2734 2189 0
+2891 685 0
+5668 3128 13
+5283 3981 37
+1447 1687 0
+2517 551 0
+605 2147 0
diff --git a/Projects/UOContent/Commands/Object Creation/Decorate.cs b/Projects/UOContent/Commands/Object Creation/Decorate.cs
index 8746fbf46..16c0da858 100644
--- a/Projects/UOContent/Commands/Object Creation/Decorate.cs
+++ b/Projects/UOContent/Commands/Object Creation/Decorate.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using CommunityToolkit.HighPerformance;
using Server.Collections;
+using Server.Engines.PlayerMurderSystem;
using Server.Engines.Quests.Haven;
using Server.Engines.Quests.Necro;
using Server.Engines.Spawners;
@@ -40,6 +41,11 @@ namespace Server.Commands
Generate("Data/Decoration/Malas", Map.Malas);
Generate("Data/Decoration/Tokuno", Map.Tokuno);
+ if (PlayerMurderSystem.BountiesEnabled)
+ {
+ Generate("Data/Decoration/BountyBoards", Map.Felucca);
+ }
+
m_Mobile.SendMessage($"World generating complete. {m_Count} items were generated.");
}
@@ -588,6 +594,15 @@ namespace Server.Commands
sp.HomeRange = Utility.ToInt32(m_Params[i].AsSpan()[++indexOf..]);
}
}
+ else if (m_Params[i].StartsWithOrdinal("WalkingRange"))
+ {
+ var indexOf = m_Params[i].IndexOfOrdinal('=');
+
+ if (indexOf >= 0)
+ {
+ sp.WalkingRange = Utility.ToInt32(m_Params[i].AsSpan()[++indexOf..]);
+ }
+ }
else if (m_Params[i].StartsWithOrdinal("Running"))
{
var indexOf = m_Params[i].IndexOfOrdinal('=');
diff --git a/Projects/UOContent/Engines/Player Murder System/BountyBoard.cs b/Projects/UOContent/Engines/Player Murder System/BountyBoard.cs
new file mode 100644
index 000000000..98538ac1b
--- /dev/null
+++ b/Projects/UOContent/Engines/Player Murder System/BountyBoard.cs
@@ -0,0 +1,90 @@
+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 (0x80000001–0xBFFFFFFF) 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;
+ }
+}
diff --git a/Projects/UOContent/Engines/Player Murder System/BountyMessage.cs b/Projects/UOContent/Engines/Player Murder System/BountyMessage.cs
new file mode 100644
index 000000000..3863f63c3
--- /dev/null
+++ b/Projects/UOContent/Engines/Player Murder System/BountyMessage.cs
@@ -0,0 +1,430 @@
+using System;
+using System.Buffers;
+using System.IO;
+using Server.Items;
+using Server.Mobiles;
+using Server.Network;
+using Server.Text;
+
+namespace Server.Engines.PlayerMurderSystem;
+
+public static class BountyMessage
+{
+ ///
+ /// Sends a synthetic container content packet (0x3C) using offset player serials.
+ /// The client displays these as bulletin board message entries. When clicked,
+ /// HandleBBRequest reverses the offset to recover the real player serial.
+ ///
+ public static void SendBountyContainerContent(NetState ns, BaseBulletinBoard board, uint syntheticSerialBase)
+ {
+ if (ns.CannotSendPackets())
+ {
+ return;
+ }
+
+ var bounties = PlayerMurderSystem.GetActiveBounties();
+ var entrySize = ns.ContainerGridLines ? 20 : 19;
+ var totalSize = 5 + bounties.Count * entrySize;
+
+ var writer = totalSize > 1024 ? new SpanWriter(totalSize) : new SpanWriter(stackalloc byte[totalSize]);
+ writer.Write((byte)0x3C); // Packet ID
+ writer.Seek(4, SeekOrigin.Current); // Length & count placeholder
+
+ var written = 0;
+
+ foreach (var (player, _) in bounties)
+ {
+ writer.Write((Serial)(syntheticSerialBase + (uint)player.Serial));
+ writer.Write((ushort)0xEB0); // BulletinMessage ItemID
+ writer.Write((byte)0); // signed, itemID offset
+ writer.Write((ushort)1); // Amount
+ writer.Write((short)0); // X
+ writer.Write((short)0); // Y
+ if (ns.ContainerGridLines)
+ {
+ writer.Write((byte)0); // Grid location
+ }
+ writer.Write(board.Serial); // Container = this board
+ writer.Write((ushort)0); // Hue
+ written++;
+ }
+
+ writer.Seek(1, SeekOrigin.Begin);
+ writer.Write((ushort)writer.BytesWritten);
+ writer.Write((ushort)written);
+ writer.Seek(0, SeekOrigin.End);
+
+ ns.Send(writer.Span);
+
+ writer.Dispose();
+ }
+
+ ///
+ /// Sends a synthetic BB message packet (header or content) for a bounty entry,
+ /// built entirely from live MurderSystem data — no real BulletinMessage item required.
+ /// Uses a resizable SpanWriter for single-pass writing with zero intermediate allocations.
+ ///
+ public static void SendBountyBBMessage(NetState ns, BaseBulletinBoard board, Serial messageSerial, PlayerMobile player,
+ int bounty, DateTime lastMurderTime, bool content)
+ {
+ if (ns.CannotSendPackets())
+ {
+ return;
+ }
+
+ // Build subject and time without heap allocation
+ using var subjectBuilder = new ValueStringBuilder(stackalloc char[32]);
+ subjectBuilder.Append(bounty);
+ subjectBuilder.Append(" gold");
+
+ using var timeBuilder = new ValueStringBuilder(stackalloc char[16]);
+ timeBuilder.Append(lastMurderTime, "MMM dd, yyyy");
+
+ // Reusable UTF-8 encoding buffer (768 = GetMaxByteCount(255), covers any WriteString call)
+ Span textBuffer = stackalloc byte[768];
+
+ // Single-pass writing with resizable SpanWriter — auto-grows if needed
+ var writer = new SpanWriter(stackalloc byte[1024], resize: true);
+ writer.Write((byte)0x71); // Packet ID
+ writer.Seek(2, SeekOrigin.Current);
+ writer.Write((byte)(content ? 0x02 : 0x01)); // Command
+ writer.Write(board.Serial);
+ writer.Write(messageSerial); // Synthetic serial — must match the 0x3C entry
+ if (!content)
+ {
+ writer.Write(Serial.Zero); // Thread serial (all root-level)
+ }
+
+ writer.WriteString(player.Name.AsSpan(), textBuffer);
+ writer.WriteString(subjectBuilder.AsSpan(), textBuffer);
+ writer.WriteString(timeBuilder.AsSpan(), textBuffer);
+
+ if (content)
+ {
+ writer.Write((short)player.Body);
+ writer.Write((short)player.Hue);
+
+ // Write equipment directly from player items (no intermediate array)
+ var equipCount = 0;
+ for (var i = 0; i < player.Items.Count; i++)
+ {
+ if (player.Items[i].Layer >= Layer.OneHanded && player.Items[i].Layer <= Layer.Mount)
+ {
+ equipCount++;
+ }
+ }
+
+ var equipLength = Math.Min(255, equipCount);
+ writer.Write((byte)equipLength);
+
+ var equipWritten = 0;
+ for (var i = 0; i < player.Items.Count && equipWritten < equipLength; i++)
+ {
+ var item = player.Items[i];
+ if (item.Layer >= Layer.OneHanded && item.Layer <= Layer.Mount)
+ {
+ writer.Write((short)item.ItemID);
+ writer.Write((short)item.Hue);
+ equipWritten++;
+ }
+ }
+
+ WriteContentLines(ref writer, textBuffer, player, bounty);
+ }
+
+ writer.WritePacketLength();
+ ns.Send(writer.Span);
+
+ writer.Dispose();
+ }
+
+ ///
+ /// Writes all content lines directly to the packet writer, avoiding intermediate
+ /// string/list allocations. Uses ValueStringBuilder for line construction and
+ /// span slicing for word wrapping.
+ ///
+ private static void WriteContentLines(ref SpanWriter writer, Span textBuffer, PlayerMobile player, int bounty)
+ {
+ var isFemale = player.Body.IsFemale;
+ var pronoun = isFemale ? "she" : "he";
+ var possessive = isFemale ? "her" : "his";
+ var objective = isFemale ? "her" : "him";
+
+ // Placeholder for line count — patched after all lines are written
+ var lineCountPos = writer.Position;
+ writer.Write((byte)0);
+ var lineCount = 0;
+
+ // Reusable builder for constructing each line
+ using var lineBuilder = new ValueStringBuilder(stackalloc char[64]);
+
+ // Title line (random, 6 variants matching uo98 bountyboard.m)
+ switch (Utility.Random(6))
+ {
+ case 0:
+ {
+ lineBuilder.Append("Bounty for");
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append("!");
+ break;
+ }
+ case 1:
+ {
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append(" must die!");
+ break;
+ }
+ case 2:
+ {
+ lineBuilder.Append("A price on ");
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append('!');
+ break;
+ }
+ case 3:
+ {
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append(" outlawed!");
+ break;
+ }
+ case 4:
+ {
+ lineBuilder.Append("Execute ");
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append('!');
+ break;
+ }
+ default:
+ {
+ lineBuilder.Append("WANTED: ");
+ lineBuilder.Append(player.RawName);
+ lineBuilder.Append('!');
+ break;
+ }
+ }
+
+ writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
+ lineCount++;
+
+ // Blank line
+ writer.WriteString(ReadOnlySpan.Empty, textBuffer, true);
+ lineCount++;
+
+ // Build main paragraph with ValueStringBuilder
+ var paraBuilder = new ValueStringBuilder(stackalloc char[256]);
+
+ // Random verb phrase (18 variants matching uo98 bountyboard.m)
+ var verb = Utility.Random(18) switch
+ {
+ 0 => "hath murdered one too many!",
+ 1 => "shall not slay again!",
+ 2 => "hath slain too many!",
+ 3 => "cannot continue to kill!",
+ 4 => "must be stopped.",
+ 5 => "is a bloodthirsty monster.",
+ 6 => "is a killer of the worst sort.",
+ 7 => "hath no conscience!",
+ 8 => "hath cowardly slain many.",
+ 9 => "must die for all our sakes.",
+ 10 => "sheds innocent blood!",
+ 11 => "must fall to preserve us.",
+ 12 => "must be taken care of.",
+ 13 => "is a thug and must die.",
+ 14 => "cannot be redeemed.",
+ 15 => "is a shameless butcher.",
+ 16 => "is a callous monster.",
+ _ => "is a cruel, casual killer."
+ };
+
+ // Random bounty intro phrase (7 variants matching uo98 bountyboard.m)
+ var intro = Utility.Random(7) switch
+ {
+ 0 => " A bounty is hereby offered",
+ 1 => " Lord British sets a price",
+ 2 => " Claim the reward! 'Tis",
+ 3 => " Lord Blackthorn set a price",
+ 4 => " The Paladins set a price",
+ 5 => " The Merchants set a price",
+ _ => " Lord British's bounty "
+ };
+
+ paraBuilder.Append("The foul scum known as ");
+ paraBuilder.Append(player.RawName);
+ paraBuilder.Append(' ');
+ paraBuilder.Append(verb);
+ paraBuilder.Append(" For ");
+ paraBuilder.Append(pronoun);
+ paraBuilder.Append(" is responsible for ");
+ paraBuilder.Append(player.Kills);
+ paraBuilder.Append(" murders. ");
+ paraBuilder.Append(intro);
+ paraBuilder.Append(" of ");
+ paraBuilder.Append(bounty);
+ paraBuilder.Append(" gold pieces for ");
+ paraBuilder.Append(possessive);
+ paraBuilder.Append(" head!");
+
+ // Word-wrap at 28 chars and write each line directly (matching uo98 bountyboard.m CONST:28)
+ WriteWordWrappedLines(ref writer, paraBuilder.AsSpan(), 28, textBuffer, ref lineCount);
+ paraBuilder.Dispose();
+
+ // Physical description
+ writer.WriteString(ReadOnlySpan.Empty, textBuffer, true);
+ lineCount++;
+
+ writer.WriteString(" A description:", textBuffer, true);
+ lineCount++;
+
+ lineBuilder.Reset();
+ lineBuilder.Append(" - ");
+ lineBuilder.Append(GetHairStyle(player.HairItemID));
+ writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
+ lineCount++;
+
+ lineBuilder.Reset();
+ lineBuilder.Append(" - ");
+ lineBuilder.Append(GetHairColor(player.HairHue));
+ lineBuilder.Append(" hair");
+ writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
+ lineCount++;
+
+ lineBuilder.Reset();
+ lineBuilder.Append(" - ");
+ lineBuilder.Append(GetSkinTone(player.Hue));
+ lineBuilder.Append(" skin");
+ writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
+ lineCount++;
+
+ // Closing instructions
+ writer.WriteString(ReadOnlySpan.Empty, textBuffer, true);
+ lineCount++;
+
+ lineBuilder.Reset();
+ lineBuilder.Append("If you kill ");
+ lineBuilder.Append(objective);
+ lineBuilder.Append(", remove the");
+ writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
+ lineCount++;
+
+ writer.WriteString("head, and give it to a guard", textBuffer, true);
+ lineCount++;
+
+ writer.WriteString("to claim your reward.", textBuffer, true);
+ lineCount++;
+
+ // Patch line count
+ var endPos = writer.Position;
+ writer.Seek(lineCountPos, SeekOrigin.Begin);
+ writer.Write((byte)Math.Min(255, lineCount));
+ writer.Seek(endPos, SeekOrigin.Begin);
+ }
+
+ ///
+ /// Word-wraps text at maxWidth characters and writes each line directly to the packet.
+ /// Uses span slicing instead of Substring to avoid allocations.
+ ///
+ private static void WriteWordWrappedLines(ref SpanWriter writer, scoped ReadOnlySpan text, int maxWidth,
+ Span textBuffer, ref int lineCount)
+ {
+ // Pre-allocate buffer for last segment (needs trailing space)
+ Span lastSegmentBuf = stackalloc char[maxWidth + 2];
+ var current = 0;
+
+ while (current < text.Length)
+ {
+ var remaining = text.Length - current;
+
+ if (remaining > maxWidth)
+ {
+ var length = maxWidth;
+
+ while (length > 0 && text[current + length] != ' ')
+ {
+ length--;
+ }
+
+ if (length == 0)
+ {
+ length = maxWidth; // hard break — no space found
+ }
+ else
+ {
+ length++; // include the space
+ }
+
+ writer.WriteString(text.Slice(current, length), textBuffer, true);
+ current += length;
+ }
+ else
+ {
+ // Last segment — append trailing space (matching original behavior)
+ text.Slice(current, remaining).CopyTo(lastSegmentBuf);
+ lastSegmentBuf[remaining] = ' ';
+ writer.WriteString(lastSegmentBuf[..(remaining + 1)], textBuffer, true);
+ current += remaining;
+ }
+
+ lineCount++;
+ }
+ }
+
+ // Maps hair item IDs to style descriptions (from uo98 bountyboard.m lookup table)
+ private static string GetHairStyle(int itemID) => itemID switch
+ {
+ 0x203B => "hair worn short",
+ 0x203C => "hair worn long",
+ 0x203D => "hair tied back",
+ 0x2044 => "a mohawk hairstyle",
+ 0x2045 => "pageboy hair",
+ 0x2046 => "hair tied in buns",
+ 0x2047 => "curly hair",
+ 0x2048 => "receding hairline",
+ 0x2049 => "hair in two pigtails",
+ 0x204A => "shaved head and topknot",
+ _ => "bald"
+ };
+
+ // Maps hair hues to color descriptions (from uo98 bountyboard.m lookup table)
+ // Human hair hues: 0x44E–0x47D. Index 0 (hue 0) = indeterminate, index 1+ maps to 0x44E+
+ private static string GetHairColor(int hue) => hue switch
+ {
+ 0 => "indeterminate color",
+ 0x44E or 0x44F or 0x450 => "white",
+ 0x451 or 0x452 or 0x453 => "graying",
+ 0x454 or 0x455 => "black",
+ 0x456 or 0x457 or 0x458 => "copper",
+ 0x459 or 0x45A or 0x45B or 0x45C => "brown",
+ 0x45D => "reddish brown",
+ 0x45E or 0x45F or 0x460 => "blonde",
+ 0x461 or 0x462 or 0x463 => "light brown",
+ 0x464 or 0x465 => "golden brown",
+ 0x466 or 0x467 or 0x468 => "golden",
+ 0x469 or 0x46A or 0x46B => "bronze",
+ 0x46C or 0x46D => "dark brown",
+ 0x46E or 0x46F => "sandy",
+ 0x470 or 0x471 or 0x472 => "honey-colored",
+ 0x473 or 0x474 or 0x475 => "red",
+ 0x476 or 0x477 or 0x478 => "nut brown",
+ 0x479 or 0x47A or 0x47B => "rich brown",
+ 0x47C or 0x47D => "very dark brown",
+ _ => "outlandishly colored"
+ };
+
+ // Maps body hues to skin tone descriptions (from uo98 bountyboard.m lookup table)
+ // Human skin hues: 0x3EA–0x422 (may have 0x8000 flag). Index 1 = 0x3EA.
+ private static string GetSkinTone(int hue) => (hue & 0x7FFF) switch
+ {
+ 0x3F1 or 0x3F2 or 0x3F8 or 0x3FF or 0x400 or 0x406 or 0x407 or 0x408
+ or 0x40D or 0x40E or 0x415 or 0x416 => "pale",
+ 0x3EA or 0x3EB or 0x3F9 or 0x3FA or 0x3FB or 0x417 => "fair",
+ 0x3F3 or 0x3F4 or 0x3FC or 0x401 or 0x402 or 0x409 or 0x40F
+ or 0x410 or 0x411 or 0x418 => "tanned",
+ 0x3EC or 0x3ED or 0x3EE or 0x3F5 or 0x403 or 0x412 or 0x413
+ or 0x419 or 0x421 => "copper",
+ 0x3EF or 0x3F0 or 0x3F6 or 0x3F7 or 0x3FD or 0x3FE or 0x404
+ or 0x405 or 0x40A or 0x40B or 0x40C or 0x414 or 0x41A
+ or 0x41B or 0x420 => "dark",
+ 0x41C or 0x41D or 0x41E => "yellow",
+ _ => "deathly"
+ };
+}
diff --git a/Projects/UOContent/Engines/Player Murder System/BountyReportMurdererGump.cs b/Projects/UOContent/Engines/Player Murder System/BountyReportMurdererGump.cs
new file mode 100644
index 000000000..c099fd2b9
--- /dev/null
+++ b/Projects/UOContent/Engines/Player Murder System/BountyReportMurdererGump.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using Server.Gumps;
+using Server.Items;
+using Server.Mobiles;
+using Server.Network;
+
+namespace Server.Engines.PlayerMurderSystem;
+
+public class BountyReportMurdererGump : DynamicGump
+{
+ private readonly List _killers;
+ private readonly PlayerMobile _victim;
+ private int _idx;
+
+ public BountyReportMurdererGump(PlayerMobile victim, List killers, int idx = 0) : base(0, 0)
+ {
+ _victim = victim;
+ _killers = killers;
+ _idx = idx;
+ }
+
+ protected override void BuildLayout(ref DynamicGumpBuilder builder)
+ {
+ builder.SetNoClose();
+ builder.SetNoResize();
+
+ builder.AddBackground(265, 205, 393, 270, 70000);
+ builder.AddImage(265, 205, 1140);
+
+ builder.AddPage();
+
+ builder.AddHtml(325, 255, 300, 60,
+ $"Would you like to report {_killers[_idx].Name} as a murderer?");
+
+ var bountyMax = Banker.GetBalance(_victim);
+
+ if (_killers[_idx].Kills >= 4 && bountyMax > 0)
+ {
+ builder.AddHtml(325, 325, 300, 60, $"Optional Bounty: [{bountyMax} max] ");
+ builder.AddImage(323, 343, 0x475);
+ builder.AddTextEntry(329, 346, 311, 16, 0, 1);
+ }
+
+ builder.AddButton(385, 395, 0x47B, 0x47D, 1);
+ builder.AddButton(465, 395, 0x478, 0x47A, 2);
+ }
+
+ public override void OnResponse(NetState state, in RelayInfo info)
+ {
+ var from = (PlayerMobile)state.Mobile;
+
+ if (info.ButtonID == 1)
+ {
+ var killer = _killers[_idx];
+
+ if (PlayerMurderSystem.ReportMurder(from, killer) && killer is PlayerMobile pk)
+ {
+ var text = info.GetTextEntry(1);
+ if (!string.IsNullOrWhiteSpace(text) && int.TryParse(text, out var requested) && requested > 0)
+ {
+ var bounty = Math.Min(requested, Banker.GetBalance(from));
+ if (bounty > 0 && Banker.Withdraw(from, bounty))
+ {
+ PlayerMurderSystem.AddBounty(pk, bounty);
+
+ pk.SendMessage(
+ $"{from.Name} has placed a bounty of {bounty} {(bounty == 1 ? "gold piece" : "gold pieces")} on your head!"
+ );
+
+ from.SendMessage($"You place a bounty of {bounty}gp on {pk.Name}'s head.");
+ }
+ }
+ }
+ }
+
+ _idx++;
+ if (_idx < _killers.Count)
+ {
+ from.SendGump(new BountyReportMurdererGump(from, _killers, _idx));
+ }
+ }
+}
diff --git a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs
index e09b94dd4..cecd7bf7e 100644
--- a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs
+++ b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs
@@ -5,7 +5,7 @@ using Server.Mobiles;
namespace Server.Engines.PlayerMurderSystem;
-[SerializationGenerator(1)]
+[SerializationGenerator(2)]
public partial class MurderContext
{
[SerializableField(0)]
@@ -28,6 +28,14 @@ public partial class MurderContext
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _pingPongs;
+ [SerializableField(4)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ private int _bounty;
+
+ [SerializableField(5)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ private DateTime _lastMurderTime;
+
private void MigrateFrom(V0Content content)
{
_shortTermElapse = content.ShortTermElapse;
@@ -37,6 +45,17 @@ public partial class MurderContext
_pingPongs = _player.Kills >= 5 ? 1 : 0;
}
+ private void MigrateFrom(V1Content content)
+ {
+ _shortTermElapse = content.ShortTermElapse;
+ _longTermElapse = content.LongTermElapse;
+ _shortTermMurders = content.ShortTermMurders;
+ _pingPongs = content.PingPongs;
+ // _bounty defaults to 0
+ // Default to now so the bounty board date display is sensible for migrated contexts
+ _lastMurderTime = Core.Now;
+ }
+
public PlayerMobile _player;
public PlayerMobile Player => _player;
diff --git a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
index ff3fc49c7..fd03f7f8a 100644
--- a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
+++ b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
@@ -6,6 +6,7 @@ using ModernUO.CodeGeneratedEvents;
using Server.Collections;
using Server.Logging;
using Server.Mobiles;
+using Server.SkillHandlers;
namespace Server.Engines.PlayerMurderSystem;
@@ -21,6 +22,9 @@ public class PlayerMurderSystem : GenericPersistence
// Only the players that are online
private static readonly HashSet _contextTerms = new(MurderContext.EqualityComparer.Default);
+ private static readonly HashSet<(Mobile, Mobile)> _recentlyReported = new();
+ private static TimeSpan _recentlyReportedDelay;
+
private static TimeSpan _shortTermMurderDuration;
private static TimeSpan _longTermMurderDuration;
@@ -31,10 +35,17 @@ public class PlayerMurderSystem : GenericPersistence
public static bool PingPongEnabled => Core.T2A && !Core.LBR;
+ public static bool BountiesEnabled { get; private set; }
+
+ private static TimeSpan _bountyExpiry;
+
public static void Configure()
{
_shortTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.shortTermMurderDuration", TimeSpan.FromHours(8));
_longTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.longTermMurderDuration", TimeSpan.FromHours(40));
+ BountiesEnabled = ServerConfiguration.GetOrUpdateSetting("murderSystem.bountiesEnabled", !Core.LBR);
+ _recentlyReportedDelay = ServerConfiguration.GetOrUpdateSetting("murderSystem.recentlyReportedDelay", TimeSpan.FromMinutes(10));
+ _bountyExpiry = ServerConfiguration.GetOrUpdateSetting("murderSystem.bountyExpiry", TimeSpan.FromDays(14));
_playerMurderPersistence = new PlayerMurderSystem();
}
@@ -173,6 +184,63 @@ public class PlayerMurderSystem : GenericPersistence
return context;
}
+ public static int GetBounty(PlayerMobile player) =>
+ GetMurderContext(player, out var context) ? context.Bounty : 0;
+
+ public static void AddBounty(PlayerMobile player, int amount)
+ {
+ if (GetMurderContext(player, out var context))
+ {
+ context.Bounty += amount;
+ }
+ }
+
+ public static void ClearBounty(PlayerMobile player)
+ {
+ if (GetMurderContext(player, out var context))
+ {
+ context.Bounty = 0;
+ }
+ }
+
+ public static int GetActiveBountyCount()
+ {
+ var neverExpire = _bountyExpiry == TimeSpan.Zero;
+ var cutoff = neverExpire ? DateTime.MinValue : Core.Now - _bountyExpiry;
+ var count = 0;
+
+ foreach (var (player, context) in _murderContexts)
+ {
+ if (player.Murderer && context.Bounty > 0 && (neverExpire || context.LastMurderTime >= cutoff))
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ // Reused across calls — safe because the server is single-threaded.
+ private static readonly List<(PlayerMobile Player, int Bounty)> _activeBountyCache = [];
+
+ public static List<(PlayerMobile Player, int Bounty)> GetActiveBounties()
+ {
+ _activeBountyCache.Clear();
+ var neverExpire = _bountyExpiry == TimeSpan.Zero;
+ var cutoff = neverExpire ? DateTime.MinValue : Core.Now - _bountyExpiry;
+
+ foreach (var (player, context) in _murderContexts)
+ {
+ if (player.Murderer && context.Bounty > 0 && (neverExpire || context.LastMurderTime >= cutoff))
+ {
+ _activeBountyCache.Add((player, context.Bounty));
+ }
+ }
+
+ _activeBountyCache.Sort(static (a, b) => b.Bounty.CompareTo(a.Bounty));
+ return _activeBountyCache;
+ }
+
public static void ManuallySetPingPong(PlayerMobile player, int pingPong)
{
var context = GetOrCreateMurderContext(player);
@@ -200,10 +268,52 @@ public class PlayerMurderSystem : GenericPersistence
context.PingPongs++;
}
+ context.LastMurderTime = Core.Now;
context.ResetKillTime();
UpdateMurderContext(context);
}
+ public static bool IsRecentlyReported(Mobile reporter, Mobile killer) =>
+ _recentlyReported.Contains((reporter, killer));
+
+ public static bool ReportMurder(PlayerMobile reporter, Mobile killer)
+ {
+ if (killer?.Deleted != false || killer is not PlayerMobile pk)
+ {
+ return false;
+ }
+
+ if (!_recentlyReported.Add((reporter, killer)))
+ {
+ return false;
+ }
+
+ Timer.DelayCall(
+ _recentlyReportedDelay,
+ static (f, k) => _recentlyReported.Remove((f, k)),
+ reporter,
+ killer
+ );
+
+ var wasMurderer = killer.Murderer;
+ OnPlayerMurder(pk);
+
+ pk.SendLocalizedMessage(1049067); // You have been reported for murder!
+
+ if (!wasMurderer && killer.Murderer)
+ {
+ pk.SendLocalizedMessage(502134); // You are now known as a murderer!
+ }
+
+ // with the introduction of PingPongs, a red can technically have 1 kill.
+ if (Stealing.SuspendOnMurder && pk.Kills == 1 && pk.NpcGuild == NpcGuild.ThievesGuild)
+ {
+ pk.SendLocalizedMessage(501562); // You have been suspended by the Thieves Guild.
+ }
+
+ return true;
+ }
+
private static void UpdateMurderContext(MurderContext context)
{
var player = context.Player;
diff --git a/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs
index 40e84d9ca..6eeb7bcf8 100644
--- a/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs
+++ b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs
@@ -5,16 +5,11 @@ using Server.Gumps;
using Server.Misc;
using Server.Mobiles;
using Server.Network;
-using Server.SkillHandlers;
namespace Server.Engines.PlayerMurderSystem;
public class ReportMurdererGump : StaticGump
{
- // Recently reported
- private static TimeSpan _recentlyReportedDelay;
- private static readonly HashSet<(Mobile, Mobile)> _recentlyReported = new();
-
private readonly List _killers;
private int _idx;
@@ -24,11 +19,6 @@ public class ReportMurdererGump : StaticGump
_idx = idx;
}
- public static void Initialize()
- {
- _recentlyReportedDelay = ServerConfiguration.GetOrUpdateSetting("murderSystem.recentlyReportedDelay", TimeSpan.FromMinutes(10));
- }
-
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
public static void OnPlayerDeathEvent(PlayerMobile m)
{
@@ -40,19 +30,16 @@ public class ReportMurdererGump : StaticGump
foreach (var ai in m.Aggressors)
{
- if (ai.Attacker.Player && ai.CanReportMurder && !ai.Reported)
+ if (ai.Attacker.Player && ai.CanReportMurder && !ai.Reported && !PlayerMurderSystem.IsRecentlyReported(m, ai.Attacker))
{
- if (!_recentlyReported.Contains((m, ai.Attacker)))
+ if (notInThievesGuild)
{
- if (notInThievesGuild)
- {
- killers ??= new List();
- killers.Add(ai.Attacker);
- }
-
- ai.Reported = true;
- ai.CanReportMurder = false;
+ killers ??= new List();
+ killers.Add(ai.Attacker);
}
+
+ ai.Reported = true;
+ ai.CanReportMurder = false;
}
if (ai.Attacker.Player && Core.Now - ai.LastCombatTime < TimeSpan.FromSeconds(30.0))
@@ -147,39 +134,7 @@ public class ReportMurdererGump : StaticGump
{
case 1:
{
- var killer = _killers[_idx];
- if (killer?.Deleted == false)
- {
- if (_recentlyReported.Add((from, killer)))
- {
- Timer.DelayCall(
- _recentlyReportedDelay,
- static (f, k) => _recentlyReported.Remove((f, k)),
- from,
- killer
- );
- }
-
- if (killer is PlayerMobile pk)
- {
- // Increment their short term murders, their kills, and reset the murder decay time
- var wasMurderer = killer.Murderer;
- PlayerMurderSystem.OnPlayerMurder(pk);
-
- pk.SendLocalizedMessage(1049067); // You have been reported for murder!
-
- if (!wasMurderer && killer.Murderer)
- {
- pk.SendLocalizedMessage(502134); // You are now known as a murderer!
- }
- // with the introduction of PingPongs, a red can technically have 1 kill.
- if (Stealing.SuspendOnMurder && pk.Kills == 1 && pk.NpcGuild == NpcGuild.ThievesGuild)
- {
- pk.SendLocalizedMessage(501562); // You have been suspended by the Thieves Guild.
- }
- }
- }
-
+ PlayerMurderSystem.ReportMurder(from, _killers[_idx]);
break;
}
case 2:
@@ -208,7 +163,14 @@ public class ReportMurdererGump : StaticGump
protected override void OnTick()
{
- _victim.SendGump(new ReportMurdererGump(_killers));
+ if (PlayerMurderSystem.BountiesEnabled && _victim is PlayerMobile pm)
+ {
+ pm.SendGump(new BountyReportMurdererGump(pm, _killers));
+ }
+ else
+ {
+ _victim.SendGump(new ReportMurdererGump(_killers));
+ }
}
}
}
diff --git a/Projects/UOContent/Items/Body Parts/Head.cs b/Projects/UOContent/Items/Body Parts/Head.cs
index f797cdd83..971c5713c 100644
--- a/Projects/UOContent/Items/Body Parts/Head.cs
+++ b/Projects/UOContent/Items/Body Parts/Head.cs
@@ -1,4 +1,6 @@
+using System;
using ModernUO.Serialization;
+using Server.Mobiles;
namespace Server.Items
{
@@ -9,7 +11,7 @@ namespace Server.Items
Tournament
}
- [SerializationGenerator(1, false)]
+ [SerializationGenerator(2, false)]
public partial class Head : Item
{
[InvalidateProperties]
@@ -22,6 +24,14 @@ namespace Server.Items
[SerializedCommandProperty(AccessLevel.GameMaster)]
private HeadType _headType;
+ [SerializableField(2)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ private PlayerMobile _bountyTarget;
+
+ [SerializableField(3)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ private DateTime _carvedTime;
+
[Constructible]
public Head(string playerName) : this(HeadType.Regular, playerName)
{
@@ -32,6 +42,7 @@ namespace Server.Items
{
_headType = headType;
_playerName = playerName;
+ _carvedTime = Core.Now;
}
public override string DefaultName
@@ -52,10 +63,21 @@ namespace Server.Items
}
}
+ // Pre-codegen legacy data (v1 only — playerName + headType)
private void Deserialize(IGenericReader reader, int version)
{
_playerName = reader.ReadString();
_headType = (HeadType)reader.ReadEncodedInt();
+ // _bountyTarget and _carvedTime default to null / DateTime.MinValue
}
+
+ private void MigrateFrom(V1Content content)
+ {
+ _playerName = content.PlayerName;
+ _headType = content.HeadType;
+ // _bountyTarget defaults to null
+ // _carvedTime defaults to DateTime.MinValue (treated as expired)
+ }
+
}
}
diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs
index d8d785445..79cf074fc 100644
--- a/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs
+++ b/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Runtime.CompilerServices;
using ModernUO.Serialization;
using Server.Collections;
@@ -163,7 +164,9 @@ namespace Server.Items
public virtual bool CheckRange(Mobile from) =>
from.AccessLevel >= AccessLevel.GameMaster || from.Map == Map && from.InRange(GetWorldLocation(), 2);
- public void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines)
+ public virtual bool HandleBBRequest(int packetID, Mobile from, SpanReader reader) => false;
+
+ public virtual void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines)
{
if (thread != null)
{
diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs
index bcf218690..ff4de5106 100644
--- a/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs
+++ b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs
@@ -58,6 +58,11 @@ namespace Server.Network
return;
}
+ if (board.HandleBBRequest(packetID, from, reader))
+ {
+ return;
+ }
+
switch (packetID)
{
case 3:
@@ -192,7 +197,16 @@ namespace Server.Network
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void UpdateLengthCounters(this string text, ref int maxLength, ref int longestTextLine, bool pad = false)
+ internal static void UpdateLengthCounters(this string text, ref int maxLength, ref int longestTextLine, bool pad = false)
+ {
+ var line = Math.Min(255, text.Length);
+ var byteCount = TextEncoding.UTF8.GetMaxByteCount(line) + (pad ? 3 : 2); // 1 + length + terminator
+ maxLength += byteCount;
+ longestTextLine = Math.Max(byteCount, longestTextLine);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void UpdateLengthCounters(this ReadOnlySpan text, ref int maxLength, ref int longestTextLine, bool pad = false)
{
var line = Math.Min(255, text.Length);
var byteCount = TextEncoding.UTF8.GetMaxByteCount(line) + (pad ? 3 : 2); // 1 + length + terminator
@@ -273,7 +287,24 @@ namespace Server.Network
writer.Dispose();
}
- private static void WriteString(this ref SpanWriter writer, string text, Span buffer, bool pad = false)
+ internal static void WriteString(this ref SpanWriter writer, string text, Span buffer, bool pad = false)
+ {
+ var tail = pad ? 2 : 1;
+ var length = Math.Min(pad ? 253 : 254, text.GetBytesUtf8(buffer));
+ writer.Write((byte)(length + tail));
+ writer.Write(buffer[..length]);
+
+ if (pad)
+ {
+ writer.Write((ushort)0); // Compensating for an old client bug
+ }
+ else
+ {
+ writer.Write((byte)0); // Terminator
+ }
+ }
+
+ internal static void WriteString(this ref SpanWriter writer, scoped ReadOnlySpan text, Span buffer, bool pad = false)
{
var tail = pad ? 2 : 1;
var length = Math.Min(pad ? 253 : 254, text.GetBytesUtf8(buffer));
diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs
index 9eb6ad7ef..534f44b64 100644
--- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs
+++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs
@@ -6,6 +6,7 @@ using Server.ContextMenus;
using Server.Engines.PartySystem;
using Server.Engines.Quests.Doom;
using Server.Engines.Quests.Haven;
+using Server.Engines.PlayerMurderSystem;
using Server.Guilds;
using Server.Misc;
using Server.Mobiles;
@@ -388,7 +389,14 @@ public partial class Corpse : Container, ICarvable
new LeftArm().MoveToWorld(Location, Map);
new RightLeg().MoveToWorld(Location, Map);
new RightArm().MoveToWorld(Location, Map);
- new Head(dead.Name).MoveToWorld(Location, Map);
+ var head = new Head(dead.Name);
+
+ if (PlayerMurderSystem.BountiesEnabled && dead is PlayerMobile bountyTarget)
+ {
+ head.BountyTarget = bountyTarget;
+ }
+
+ head.MoveToWorld(Location, Map);
SetFlag(CorpseFlag.Carved, true);
diff --git a/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v2.json b/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v2.json
new file mode 100644
index 000000000..fee2646e5
--- /dev/null
+++ b/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v2.json
@@ -0,0 +1,48 @@
+{
+ "version": 2,
+ "type": "Server.Engines.PlayerMurderSystem.MurderContext",
+ "properties": [
+ {
+ "name": "ShortTermElapse",
+ "type": "System.TimeSpan",
+ "rule": "PrimitiveTypeMigrationRule"
+ },
+ {
+ "name": "LongTermElapse",
+ "type": "System.TimeSpan",
+ "rule": "PrimitiveTypeMigrationRule"
+ },
+ {
+ "name": "ShortTermMurders",
+ "type": "int",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ },
+ {
+ "name": "PingPongs",
+ "type": "int",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ },
+ {
+ "name": "Bounty",
+ "type": "int",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ },
+ {
+ "name": "LastMurderTime",
+ "type": "System.DateTime",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Projects/UOContent/Migrations/Server.Items.BountyBoard.v0.json b/Projects/UOContent/Migrations/Server.Items.BountyBoard.v0.json
new file mode 100644
index 000000000..0f6c9ba87
--- /dev/null
+++ b/Projects/UOContent/Migrations/Server.Items.BountyBoard.v0.json
@@ -0,0 +1,4 @@
+{
+ "version": 0,
+ "type": "Server.Items.BountyBoard"
+}
\ No newline at end of file
diff --git a/Projects/UOContent/Migrations/Server.Items.Head.v2.json b/Projects/UOContent/Migrations/Server.Items.Head.v2.json
new file mode 100644
index 000000000..0aaf53c22
--- /dev/null
+++ b/Projects/UOContent/Migrations/Server.Items.Head.v2.json
@@ -0,0 +1,32 @@
+{
+ "version": 2,
+ "type": "Server.Items.Head",
+ "properties": [
+ {
+ "name": "PlayerName",
+ "type": "string",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ },
+ {
+ "name": "HeadType",
+ "type": "Server.Items.HeadType",
+ "rule": "EnumMigrationRule"
+ },
+ {
+ "name": "BountyTarget",
+ "type": "Server.Mobiles.PlayerMobile",
+ "rule": "SerializableInterfaceMigrationRule"
+ },
+ {
+ "name": "CarvedTime",
+ "type": "System.DateTime",
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs
index f180f9935..ad63338fd 100644
--- a/Projects/UOContent/Mobiles/Guards/BaseGuard.cs
+++ b/Projects/UOContent/Mobiles/Guards/BaseGuard.cs
@@ -1,7 +1,9 @@
using System;
using System.Runtime.CompilerServices;
using ModernUO.Serialization;
+using Server.Engines.PlayerMurderSystem;
using Server.Items;
+using Server.Misc;
namespace Server.Mobiles;
@@ -141,6 +143,58 @@ public abstract partial class BaseGuard : Mobile
public abstract void NonLethalAttack(Mobile target);
+ public override bool OnDragDrop(Mobile from, Item dropped)
+ {
+ if (PlayerMurderSystem.BountiesEnabled && dropped is Head head && head.PlayerName != null)
+ {
+ var target = head.BountyTarget;
+
+ if (target == null || Core.Now - head.CarvedTime > TimeSpan.FromHours(24))
+ {
+ SayNonBountyHeadResponse();
+ head.Delete();
+ return true;
+ }
+
+ Say(500670); // Ah, a head! Let me check to see if there is a bounty on this.
+ head.Delete();
+ ClaimBounty(from, target);
+ return true;
+ }
+
+ return base.OnDragDrop(from, dropped);
+ }
+
+ private void ClaimBounty(Mobile from, PlayerMobile target)
+ {
+ var bounty = PlayerMurderSystem.GetBounty(target);
+
+ if (bounty > 0)
+ {
+ PlayerMurderSystem.ClearBounty(target);
+ Banker.Deposit(from, bounty);
+ Titles.AwardKarma(from, 2000, true);
+
+ Say(1042855, $"{target.Name}\t{bounty}"); // The bounty on ~1_PLAYER_NAME~ was ~2_AMOUNT~ gold, and has been credited to your account.
+ }
+ else
+ {
+ Say(1042854, target.Name); // There was no bounty on ~1_PLAYER_NAME~.
+ }
+ }
+
+ private void SayNonBountyHeadResponse()
+ {
+ if (Utility.Random(5) == 0)
+ {
+ Say(500661 + Utility.Random(9)); // 500661–500669: silly guard responses
+ }
+ else
+ {
+ Say(500654 + Utility.Random(7)); // 500654–500660: normal guard responses
+ }
+ }
+
[AfterDeserialization]
private void AfterDeserialization()
{