feat(bridge): protocol 8 — the shard reads its own client files (Asset Bridge cutover, 1 of 5) #36

Merged
whitlocktech merged 20 commits from edge into main 2026-09-14 23:09:57 +00:00
3 changed files with 282 additions and 45 deletions
Showing only changes of commit 577688b993 - Show all commits

View File

@@ -406,11 +406,123 @@ namespace Server.Custom.Bridge
return false; return false;
} }
int actions = ActionsOf(translated, fileType);
if (action >= actions)
{
// §4.10, measured in phase 6: this is the never-sweep rule again, one axis over.
// A body's slots are contiguous and the next body's begin immediately after them,
// so `index + action * 5` past the ceiling addresses ANOTHER BODY'S action — a
// real record, at a real offset, that every check below passes. Measured on this
// client: of 795 legacy bodies, 643 return a fully validated picture one action
// past their band and **452 of those are byte-identical to body+1's action 0**.
// Body 1 action 22 is an ettin; body 3 action 22 is an imp. Nothing downstream
// can tell, which is why the refusal has to be here.
reason = "body " + body + " has " + actions + " actions in file type " + fileType
+ "; action " + action + " belongs to the next body";
return false;
}
index = AnimIndexOf(translated, fileType) + (action * 5) + direction; index = AnimIndexOf(translated, fileType) + (action * 5) + direction;
return true; return true;
} }
/// <summary>
/// How many actions the index reserves for a body — the only safe ceiling, and it is
/// the banding rather than the library's own answer.
///
/// <c>Animations.GetAnimLength</c> exists and looks like the right source. It is not:
/// for a body reaching file type 5 as id 34 it answers **22** while
/// <see cref="AnimIndexOf"/> puts that body in the 65-slot band, which is **13**. The
/// two disagree on exactly one body of this client (reached by translation from body
/// 276), and taking the larger number is nine actions of somebody else's art. So the
/// count is derived from the same arithmetic that produces the offset, in the same
/// file, where the two cannot drift apart.
/// </summary>
public static bool ActionCount(int body, out int actions, out int fileType, out string reason)
{
reason = null;
actions = 0;
fileType = 0;
if (body <= 0)
{
reason = "body " + body + " is not addressable";
return false;
}
int translated = body;
int hue = 0;
try
{
Animations.Translate(ref translated, ref hue);
fileType = BodyConverter.Convert(ref translated);
}
catch (Exception e)
{
reason = "body.def/bodyconv.def lookup failed: " + e.GetType().Name;
return false;
}
if (AnimDataPath(fileType) == null)
{
reason = "bodyconv sends body " + body + " to file type " + fileType
+ ", which this client does not have";
return false;
}
actions = ActionsOf(translated, fileType);
return true;
}
/// <summary>
/// The banding of <see cref="AnimIndexOf"/>, read as an action count: a body's slots
/// are five directions per action, so the band size divided by five is how many
/// actions it owns.
/// </summary>
private static int ActionsOf(int body, int fileType)
{
return SlotsOf(body, fileType) / 5;
}
/// <summary>
/// How many index slots <see cref="AnimIndexOf"/>'s arithmetic gives this body. The
/// bands are transcribed there and their sizes here, from the same source and in the
/// same order, because a ceiling that disagrees with an offset is worse than no
/// ceiling at all.
/// </summary>
private static int SlotsOf(int body, int fileType)
{
switch (fileType)
{
case 2:
return body < 200 ? 110 : 65;
case 3:
if (body < 300)
return 65;
return body < 400 ? 110 : 175;
case 5:
// Body 34's exclusion again — it is in the second band here, so it owns 13
// actions and not 22. This is the one body `GetAnimLength` is wrong about.
if (body < 200 && body != 34)
return 110;
return body < 400 ? 65 : 175;
default: // 1 and 4 share their banding
if (body < 200)
return 110;
return body < 400 ? 65 : 175;
}
}
/// <summary> /// <summary>
/// <c>Animations.GetFileIndex</c>'s own arithmetic, which is private. The banding is /// <c>Animations.GetFileIndex</c>'s own arithmetic, which is private. The banding is
/// per file type and the boundaries differ between them, so this is transcribed rather /// per file type and the boundaries differ between them, so this is transcribed rather

View File

@@ -85,8 +85,13 @@ namespace Server.Custom.Bridge
/// stock client is 235 new sprites and two of them player-character bodies; and the /// stock client is 235 new sprites and two of them player-character bodies; and the
/// player-body set no longer carries ghost ids. Every client file is byte-identical /// player-body set no longer carries ghost ids. Every client file is byte-identical
/// and the answer is different, which is precisely what this number exists to say. /// and the answer is different, which is precisely what this number exists to say.
///
/// **3** — phase 6 (§4.10, §11.2). A body with no art at action 0 is catalogued at
/// the first action that has any, and its key names that action. 74 more bodies on a
/// stock client, no existing key's bytes changed — but a body that was absent is now
/// a row, which is the same "unchanged input, different answer" this number covers.
/// </summary> /// </summary>
public const int EXTRACTOR_VERSION = 2; public const int EXTRACTOR_VERSION = 3;
// ── the one slot (§3.2) ────────────────────────────────────────────────────────────── // ── the one slot (§3.2) ──────────────────────────────────────────────────────────────

View File

@@ -14,11 +14,20 @@ namespace Server.Custom.Bridge
/// ///
/// One thumbnail per creature body: the working set that makes a bestiary, a marketplace /// One thumbnail per creature body: the working set that makes a bestiary, a marketplace
/// listing and a character sheet render. Everything deeper — every action, every frame — /// listing and a character sheet render. Everything deeper — every action, every frame —
/// is the same addressing scheme at a deeper key, fetched on demand in a later phase; this /// is the same addressing scheme at a deeper key and is **not served** (§11.2, phase 6:
/// is the set that is worth importing before anything asks for it, because on this /// the site shows still pictures, so frames wait for a consumer that wants them). This is
/// machine's client it is **1,022 sprites at about a kilobyte each** — 787 out of the /// the set that is worth importing before anything asks for it, because on this machine's
/// legacy `anim*.mul` files and, since phase 4, 235 more out of `AnimationFrame*.uop`, /// client it is **1,096 sprites at about a kilobyte each** — 787 out of the legacy
/// which ServUO's vendored decoder never opens (§4.3, §4.9). /// `anim*.mul` files, 235 more out of `AnimationFrame*.uop` since phase 4 (§4.3, §4.9),
/// and 74 more since phase 6, which have no art at action 0 and real art deeper.
///
/// ── **One picture per body, at the first action that has one** ──
///
/// A key carries the action it came from — `body/820/a23` for a horse whose action 0 is
/// empty — so the catalogue is still exactly one row per body, and the row says which
/// picture it is. What it never does is decode an action the walk did not choose: a fetch
/// for `body/820/a0` is `unsupported`, not a second attempt, because the slots past a
/// body's band belong to the next body and every check passes on them (§4.10).
/// ///
/// Two request kinds, which are §6's two stages for assets rather than for sources: /// Two request kinds, which are §6's two stages for assets rather than for sources:
/// ///
@@ -89,8 +98,22 @@ namespace Server.Custom.Bridge
/// <summary>Bodies are addressable to 2047; the sweep behind §4.8 covered exactly this.</summary> /// <summary>Bodies are addressable to 2047; the sweep behind §4.8 covered exactly this.</summary>
private const int MaxBody = 2047; private const int MaxBody = 2047;
/// <summary>The catalogue is first frames only. Deep keys are phase 6.</summary> /// <summary>
private const int CatalogAction = 0; /// The action a thumbnail comes from when the body has one, which is nearly always.
/// Everything deeper than a first frame is deferred — see §11.2.
/// </summary>
private const int PreferredAction = 0;
/// <summary>
/// How far the fallback looks for a body with no art at <see cref="PreferredAction"/>.
///
/// 35 because that is the largest band any file type gives a body, so an action beyond
/// it is not something the client's own layout can name. The legacy arm is bounded
/// tighter still and per body, by <see cref="BridgeAssetValidator.ActionCount"/> —
/// this is only the scan's outer stop, and it is the UOP arm's real one, where an
/// action is a named entry rather than an offset.
/// </summary>
private const int MaxAction = 35;
public static void Initialize() public static void Initialize()
{ {
@@ -110,6 +133,14 @@ namespace Server.Custom.Bridge
{ {
public string Key; public string Key;
public int Body; public int Body;
/// <summary>
/// Which action this body's thumbnail came from — <see cref="PreferredAction"/>
/// for all but 74 bodies on this client, and on the wire because the key names it
/// (§5, §11.2). A consumer that assumes `a0` would build a dead URL for a horse.
/// </summary>
public int Action;
public int Direction; public int Direction;
public int FileType; public int FileType;
public string Sha256; public string Sha256;
@@ -130,8 +161,15 @@ namespace Server.Custom.Bridge
private sealed class Catalog private sealed class Catalog
{ {
public string Id; public string Id;
public readonly Dictionary<string, Sprite> ByKey =
new Dictionary<string, Sprite>(StringComparer.Ordinal); /// <summary>
/// Keyed by **body**, not by asset key, since phase 6: a body's key now carries
/// the action its picture came from, so the key cannot be spelled until the body
/// has been resolved. A fetch arrives holding a key and has to reach the same
/// sprite, which it does by parsing the body out of it and comparing.
/// </summary>
public readonly Dictionary<int, Sprite> ByBody = new Dictionary<int, Sprite>();
public readonly List<Sprite> Order = new List<Sprite>(); public readonly List<Sprite> Order = new List<Sprite>();
/// <summary>The next body the scan has yet to look at.</summary> /// <summary>The next body the scan has yet to look at.</summary>
@@ -264,6 +302,7 @@ namespace Server.Custom.Bridge
item.Append(",\"width\":").Append(sprite.Width.ToString(CultureInfo.InvariantCulture)); item.Append(",\"width\":").Append(sprite.Width.ToString(CultureInfo.InvariantCulture));
item.Append(",\"height\":").Append(sprite.Height.ToString(CultureInfo.InvariantCulture)); item.Append(",\"height\":").Append(sprite.Height.ToString(CultureInfo.InvariantCulture));
item.Append(",\"body\":").Append(sprite.Body.ToString(CultureInfo.InvariantCulture)); item.Append(",\"body\":").Append(sprite.Body.ToString(CultureInfo.InvariantCulture));
item.Append(",\"action\":").Append(sprite.Action.ToString(CultureInfo.InvariantCulture));
item.Append(",\"direction\":").Append(sprite.Direction.ToString(CultureInfo.InvariantCulture)); item.Append(",\"direction\":").Append(sprite.Direction.ToString(CultureInfo.InvariantCulture));
item.Append(",\"source\":\"").Append(sprite.Source).Append('"'); item.Append(",\"source\":\"").Append(sprite.Source).Append('"');
item.Append('}'); item.Append('}');
@@ -413,9 +452,9 @@ namespace Server.Custom.Bridge
/// </summary> /// </summary>
private static string Render(Catalog catalog, Readers readers, string key) private static string Render(Catalog catalog, Readers readers, string key)
{ {
int body; int body, action;
if (!TryParseKey(key, out body)) if (!TryParseKey(key, out body, out action))
{ {
var bad = new StringBuilder(96); var bad = new StringBuilder(96);
bad.Append("{\"key\":"); bad.Append("{\"key\":");
@@ -437,12 +476,26 @@ namespace Server.Custom.Bridge
return item.ToString(); return item.ToString();
} }
if (sprite.Action != action)
{
// The body has a picture, but not at the action this key names. Two ways to get
// here and both are the caller's: an old manifest that catalogued this body at
// `a0` before the client was patched, or a key someone built by assuming the
// action. Neither is served — decoding the asked-for action instead would be
// §4.10's wrong picture, arrived at politely.
item.Append(",\"status\":\"unsupported\"");
item.Append(",\"action\":").Append(sprite.Action.ToString(CultureInfo.InvariantCulture));
item.Append('}');
return item.ToString();
}
item.Append(",\"status\":\"ok\""); item.Append(",\"status\":\"ok\"");
item.Append(",\"sha256\":\"").Append(sprite.Sha256).Append('"'); item.Append(",\"sha256\":\"").Append(sprite.Sha256).Append('"');
item.Append(",\"bytes\":").Append(sprite.Png.Length.ToString(CultureInfo.InvariantCulture)); item.Append(",\"bytes\":").Append(sprite.Png.Length.ToString(CultureInfo.InvariantCulture));
item.Append(",\"width\":").Append(sprite.Width.ToString(CultureInfo.InvariantCulture)); item.Append(",\"width\":").Append(sprite.Width.ToString(CultureInfo.InvariantCulture));
item.Append(",\"height\":").Append(sprite.Height.ToString(CultureInfo.InvariantCulture)); item.Append(",\"height\":").Append(sprite.Height.ToString(CultureInfo.InvariantCulture));
item.Append(",\"body\":").Append(sprite.Body.ToString(CultureInfo.InvariantCulture)); item.Append(",\"body\":").Append(sprite.Body.ToString(CultureInfo.InvariantCulture));
item.Append(",\"action\":").Append(sprite.Action.ToString(CultureInfo.InvariantCulture));
item.Append(",\"direction\":").Append(sprite.Direction.ToString(CultureInfo.InvariantCulture)); item.Append(",\"direction\":").Append(sprite.Direction.ToString(CultureInfo.InvariantCulture));
item.Append(",\"source\":\"").Append(sprite.Source).Append('"'); item.Append(",\"source\":\"").Append(sprite.Source).Append('"');
item.Append(",\"png\":\"").Append(Convert.ToBase64String(sprite.Png)).Append("\"}"); item.Append(",\"png\":\"").Append(Convert.ToBase64String(sprite.Png)).Append("\"}");
@@ -459,13 +512,11 @@ namespace Server.Custom.Bridge
/// </summary> /// </summary>
private static Sprite Resolve(Catalog catalog, Readers readers, int body) private static Sprite Resolve(Catalog catalog, Readers readers, int body)
{ {
string key = Key(body);
lock (_sync) lock (_sync)
{ {
Sprite cached; Sprite cached;
if (catalog.ByKey.TryGetValue(key, out cached)) if (catalog.ByBody.TryGetValue(body, out cached))
return cached; return cached;
} }
@@ -473,38 +524,88 @@ namespace Server.Custom.Bridge
? BridgeConfig.AssetPlayerDirection ? BridgeConfig.AssetPlayerDirection
: BridgeConfig.AssetCreatureDirection; : BridgeConfig.AssetCreatureDirection;
// Legacy first, always. The vendored decoder is what 787 of this client's bodies come Sprite sprite = ResolveAny(readers, body, direction);
// out of, it is what phase 3 measured, and the UOP packages hold a different and
// mostly disjoint set (measured: of the 244 bodies they carry, 8 also have legacy
// art). So this is a fallback rather than a choice, and no body changes reader while
// a client sits still.
Sprite sprite = ResolveLegacy(key, readers, body, direction)
?? ResolveUop(key, readers, body, direction);
if (sprite == null) if (sprite == null)
return null; return null;
lock (_sync) lock (_sync)
{ {
if (!catalog.ByKey.ContainsKey(key)) if (!catalog.ByBody.ContainsKey(body))
{ {
catalog.ByKey[key] = sprite; catalog.ByBody[body] = sprite;
catalog.Order.Add(sprite); catalog.Order.Add(sprite);
} }
return catalog.ByKey[key]; return catalog.ByBody[body];
} }
} }
/// <summary>
/// One body's thumbnail: action 0 if it has one, otherwise the first action that does.
///
/// ── **Why there is a fallback at all** ──
///
/// Through phase 5 a body with no art at action 0 was simply absent, and on this
/// client **74 bodies are in exactly that state while carrying real art deeper** — 66
/// of them UOP, 8 legacy. Body 820's first drawn action is 23, and it is a horse.
/// They rendered as text on the bestiary for want of looking one action further.
///
/// ── **Why the key says which action it is** ──
///
/// The fallback's picture is `body/820/a23`, not `body/820/a0`. Naming it `a0` would
/// have been fewer changes downstream and a key that lies about its content, which is
/// the failure this protocol keeps meeting from other directions (§4.5, §4.8, §11.1).
///
/// ── **Why the ceiling is not a detail** ──
///
/// Scanning actions is the one thing that can walk off the end of a body's slots, and
/// the slots immediately after a body's are the **next body's**. Measured in phase 6:
/// 643 of 795 legacy bodies return a fully validated, correctly-sized picture one
/// action past their band, and 452 of those are byte-identical to body+1's action 0.
/// <see cref="BridgeAssetValidator.ResolveAnimation"/> refuses past the ceiling, so
/// this walk cannot produce one — see §4.10.
/// </summary>
private static Sprite ResolveAny(Readers readers, int body, int direction)
{
int actions, fileType;
string reason;
// The legacy ceiling. A body the legacy path cannot place at all still gets the UOP
// arm below, where an action is a named entry rather than an offset into a band.
if (!BridgeAssetValidator.ActionCount(body, out actions, out fileType, out reason))
actions = 0;
for (int action = PreferredAction; action < MaxAction; action++)
{
// Legacy first, always. The vendored decoder is what 787 of this client's bodies
// come out of, it is what phase 3 measured, and the UOP packages hold a different
// and mostly disjoint set (measured: of the 244 bodies they carry, 8 also have
// legacy art). So this is a fallback rather than a choice, and no body changes
// reader while a client sits still.
Sprite sprite = action < actions
? ResolveLegacy(Key(body, action), readers, body, action, direction)
: null;
if (sprite == null)
sprite = ResolveUop(Key(body, action), readers, body, action, direction);
if (sprite != null)
return sprite;
}
return null;
}
/// <summary> /// <summary>
/// ServUO's vendored <c>Animations</c> over <c>anim*.mul</c>, behind §4.5's validator. /// ServUO's vendored <c>Animations</c> over <c>anim*.mul</c>, behind §4.5's validator.
/// </summary> /// </summary>
private static Sprite ResolveLegacy(string key, Readers readers, int body, int direction) private static Sprite ResolveLegacy(string key, Readers readers, int body, int action, int direction)
{ {
int fileType, at; int fileType, at;
string reason; string reason;
if (!BridgeAssetValidator.ResolveAnimation(body, CatalogAction, direction, if (!BridgeAssetValidator.ResolveAnimation(body, action, direction,
out fileType, out at, out reason)) out fileType, out at, out reason))
return null; return null;
@@ -534,12 +635,12 @@ namespace Server.Custom.Bridge
try try
{ {
return Decode(key, body, direction, fileType); return Decode(key, body, action, direction, fileType);
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine("[Bridge] catalogue: body {0}: {1}: {2}", Console.WriteLine("[Bridge] catalogue: body {0} action {1}: {2}: {3}",
body, e.GetType().Name, e.Message); body, action, e.GetType().Name, e.Message);
return null; return null;
} }
} }
@@ -558,9 +659,9 @@ namespace Server.Custom.Bridge
/// hash of a name carrying the body id, and the payload repeats that id in its own /// hash of a name carrying the body id, and the payload repeats that id in its own
/// header for <see cref="BridgeUop.Group.TryOpen"/> to check. A miss is a miss. /// header for <see cref="BridgeUop.Group.TryOpen"/> to check. A miss is a miss.
/// </summary> /// </summary>
private static Sprite ResolveUop(string key, Readers readers, int body, int direction) private static Sprite ResolveUop(string key, Readers readers, int body, int action, int direction)
{ {
ulong hash = BridgeUop.HashOf(body, CatalogAction); ulong hash = BridgeUop.HashOf(body, action);
byte[] payload = null; byte[] payload = null;
string reason = null; string reason = null;
@@ -574,8 +675,8 @@ namespace Server.Custom.Bridge
if (!package.TryRead(hash, out payload, out reason)) if (!package.TryRead(hash, out payload, out reason))
{ {
Console.WriteLine("[Bridge] catalogue: body {0} in {1}: {2}", Console.WriteLine("[Bridge] catalogue: body {0} action {1} in {2}: {3}",
body, BridgeUop.PackageName(n), reason); body, action, BridgeUop.PackageName(n), reason);
return null; return null;
} }
@@ -589,7 +690,8 @@ namespace Server.Custom.Bridge
if (!BridgeUop.Group.TryOpen(payload, body, out group, out reason)) if (!BridgeUop.Group.TryOpen(payload, body, out group, out reason))
{ {
Console.WriteLine("[Bridge] catalogue: body {0} uop: {1}", body, reason); Console.WriteLine("[Bridge] catalogue: body {0} action {1} uop: {2}",
body, action, reason);
return null; return null;
} }
@@ -607,7 +709,8 @@ namespace Server.Custom.Bridge
// exactly the same condition — so it is absent, silently. Anything else is a // exactly the same condition — so it is absent, silently. Anything else is a
// record this reader refused, and that is worth a line. // record this reader refused, and that is worth a line.
if (!empty) if (!empty)
Console.WriteLine("[Bridge] catalogue: body {0} uop: {1}", body, reason); Console.WriteLine("[Bridge] catalogue: body {0} action {1} uop: {2}",
body, action, reason);
return null; return null;
} }
@@ -621,6 +724,7 @@ namespace Server.Custom.Bridge
{ {
Key = key, Key = key,
Body = body, Body = body,
Action = action,
Direction = direction, Direction = direction,
FileType = 0, FileType = 0,
Png = png, Png = png,
@@ -631,14 +735,14 @@ namespace Server.Custom.Bridge
}; };
} }
private static Sprite Decode(string key, int body, int direction, int fileType) private static Sprite Decode(string key, int body, int action, int direction, int fileType)
{ {
int hue = 0; int hue = 0;
// `preserveHue: false` — the catalogue is the creature's own art, and a body-level hue // `preserveHue: false` — the catalogue is the creature's own art, and a body-level hue
// from Body.def belongs to a specific mob rather than to the species. §5's key scheme // from Body.def belongs to a specific mob rather than to the species. §5's key scheme
// is where a hued variant is expressed (`static/3922/h33`), not here. // is where a hued variant is expressed (`static/3922/h33`), not here.
Frame[] frames = Animations.GetAnimation(body, CatalogAction, direction, ref hue, false, true); Frame[] frames = Animations.GetAnimation(body, action, direction, ref hue, false, true);
if (frames == null || frames.Length == 0 || frames[0] == null) if (frames == null || frames.Length == 0 || frames[0] == null)
return null; return null;
@@ -657,6 +761,7 @@ namespace Server.Custom.Bridge
{ {
Key = key, Key = key,
Body = body, Body = body,
Action = action,
Direction = direction, Direction = direction,
FileType = fileType, FileType = fileType,
Png = png, Png = png,
@@ -758,20 +863,28 @@ namespace Server.Custom.Bridge
// ── keys, cursors and the source id ────────────────────────────────────────────────── // ── keys, cursors and the source id ──────────────────────────────────────────────────
private static string Key(int body) private static string Key(int body, int action)
{ {
return "body/" + body.ToString(CultureInfo.InvariantCulture) return "body/" + body.ToString(CultureInfo.InvariantCulture)
+ "/a" + CatalogAction.ToString(CultureInfo.InvariantCulture); + "/a" + action.ToString(CultureInfo.InvariantCulture);
} }
/// <summary> /// <summary>
/// `body/&lt;id&gt;/a0`, and nothing else in this phase. A deeper key /// `body/&lt;id&gt;/a&lt;n&gt;`, and nothing else in this phase. A deeper key
/// (`body/400/a2/f3`) is well-formed under §5 and simply not served yet, so it comes /// (`body/400/a2/f3`) is well-formed under §5 and simply not served, so it comes back
/// back `unsupported` rather than being silently read as its own first frame. /// `unsupported` rather than being silently read as its own first frame.
///
/// The action is parsed rather than required to be zero — 74 of this client's bodies
/// are catalogued at a different one (§11.2) — but a parsed action is not an accepted
/// one. <see cref="Render"/> serves a key only when it is the key the catalogue itself
/// chose for that body, which is what keeps §4.10's ceiling from being reachable
/// through a request: nothing the website can ask makes this decode an action the
/// catalogue did not already pick.
/// </summary> /// </summary>
private static bool TryParseKey(string key, out int body) private static bool TryParseKey(string key, out int body, out int action)
{ {
body = 0; body = 0;
action = -1;
if (key == null) if (key == null)
return false; return false;
@@ -787,7 +900,14 @@ namespace Server.Custom.Bridge
if (body < 1 || body > MaxBody) if (body < 1 || body > MaxBody)
return false; return false;
return parts[2] == "a" + CatalogAction.ToString(CultureInfo.InvariantCulture); if (parts[2].Length < 2 || parts[2][0] != 'a')
return false;
if (!Int32.TryParse(parts[2].Substring(1), NumberStyles.None,
CultureInfo.InvariantCulture, out action))
return false;
return action >= 0 && action < MaxAction;
} }
private static int ParseBodyCursor(string cursor) private static int ParseBodyCursor(string cursor)