Fix ADV config opacity and text lifetime
This commit is contained in:
@@ -226,11 +226,36 @@ public class AdvTextOpsTests
|
||||
Assert.Equal((53, 10, 495, 60), CursorAndBounds(history.GetLayoutSnapshot(8)));
|
||||
history.ResetLayout(9);
|
||||
Assert.Equal((10, 10, 250, 368), CursorAndBounds(history.GetLayoutSnapshot(9)));
|
||||
Assert.Equal(new[] { 1 }, history.LayoutsCoveredByTextObjectErase(0xd6d8, 0x1f4));
|
||||
Assert.Equal(new[] { 7 }, history.LayoutsCoveredByTextObjectErase(0x7d0, 0x1f4));
|
||||
|
||||
static (int X, int Y, int Right, int Bottom) CursorAndBounds(AdvTextLayoutSnapshot layout)
|
||||
=> (layout.CursorX, layout.CursorY, layout.Right, layout.Bottom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullNativeGlyphRangeEraseClearsDetachedLayoutPresentation()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
static Operand I(long value) => new(0, value);
|
||||
var script = ScriptAssembler.Assemble(table, "ADV_GLYPH_ERASE",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x70, new[] { I(1), I(800), I(160), I(0), I(430) }),
|
||||
(0x213, new[] { I(1), I(0xd6d8), I(0x1f4) }),
|
||||
(0x1f7, new[] { I(0xd6d8), I(1) }), // partial erase cannot clear collapsed text
|
||||
(0x1f7, new[] { I(0xd6d8), I(0x1f4) }), // SC0000 transition teardown
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { 1 }, host.ClearedTextLayouts);
|
||||
Assert.Null(vm.Gfx.TryGet(1)); // 0x213 configures the ADV layout, not retained gfx handle 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorConfigurationReachesHost()
|
||||
{
|
||||
|
||||
@@ -69,6 +69,25 @@ public class HistoryPresentationOpsTests
|
||||
Assert.Equal((0L, 60000L), Assert.Single(host.PresentedRanges));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageWindowAlphaSetterImmediatelyFeedsThePairedGetter()
|
||||
{
|
||||
var script = ScriptAssembler.Assemble(Table, "MESSAGE_WINDOW_ALPHA",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x141, new[] { I(7) }),
|
||||
(0x131, new[] { G(0x100) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, Table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(7, host.MessageWindowAlphaSetting);
|
||||
Assert.Equal(7, vm.Globals[0x100]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetLayoutClearsItsPreviouslyBoundHostPresentation()
|
||||
{
|
||||
|
||||
@@ -101,6 +101,35 @@ public class RenderObjectBlendTests
|
||||
Assert.Equal(255, g.SnapshotVisibleObjects().Single(x => x.Handle == idle).Alpha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvCallbackRebuild_PostBindStaticAlphaControlsBackingOpacity()
|
||||
{
|
||||
const long backing = 0xd2f0;
|
||||
var g = new GfxState();
|
||||
g.SetSurface(0x11, resId: 0x1234, colorKey: -1);
|
||||
|
||||
// CALLBACK_SETTING erases the old ADV family, recreates the backing from SO001, then applies
|
||||
// (16-MesWinAlpha)<<4 through static mode-0 op 0x203.
|
||||
g.EraseRange(backing, 7);
|
||||
g.BindDraw(backing, 0x11, 0, 0, 800, 227, 0, 373);
|
||||
g.SetStaticObjectColorResolved(backing, mode: 0, alpha: 0x80, rgb: 0x000000);
|
||||
|
||||
var rebuilt = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(0x80, rebuilt.Alpha);
|
||||
Assert.Equal(0x000000, rebuilt.Tint);
|
||||
Assert.True(rebuilt.MultiplyTint);
|
||||
|
||||
// A later draw-texture bind starts a new presentation lifetime and clears the post-bind latch.
|
||||
g.BindDraw(backing, 0x11, 0, 0, 800, 227, 0, 373);
|
||||
Assert.Equal(255, g.SnapshotVisibleObjects().Single().Alpha);
|
||||
|
||||
// Opening CONFIG again repeats the erase/rebuild sequence and must accept the next setting too.
|
||||
g.EraseRange(backing, 7);
|
||||
g.BindDraw(backing, 0x11, 0, 0, 800, 227, 0, 373);
|
||||
g.SetStaticObjectColorResolved(backing, mode: 0, alpha: 0x20, rgb: 0x000000);
|
||||
Assert.Equal(0x20, g.SnapshotVisibleObjects().Single().Alpha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransitionCleanup_Mode2ToMode0PreservedWhite_RemainsIdentity()
|
||||
{
|
||||
|
||||
@@ -99,6 +99,7 @@ internal class RecordingHost : IHost
|
||||
ActiveHistoryRenders.Clear();
|
||||
}
|
||||
public int MessageWindowAlphaSetting { get; set; }
|
||||
public void SetMessageWindowAlphaSetting(int value) => MessageWindowAlphaSetting = value;
|
||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
||||
public void CopySurfaceRect(SurfaceRectCopy copy) => SurfaceCopies.Add(copy);
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
|
||||
@@ -52,6 +52,7 @@ public interface IHost
|
||||
// recording re-enable at exit ends that presentation and drops every bound target layout.
|
||||
void EndTextHistoryPresentation() { }
|
||||
int MessageWindowAlphaSetting => 0;
|
||||
void SetMessageWindowAlphaSetting(int value) { }
|
||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||
|
||||
@@ -108,6 +108,9 @@ public sealed class AdvTextHistory
|
||||
public int CursorY;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
public long WaitIndicatorObjectHandle = -1;
|
||||
public long TextObjectRangeFirst = -1;
|
||||
public long TextObjectRangeCount;
|
||||
}
|
||||
|
||||
private readonly List<AdvTextHistoryRecord> _records = new();
|
||||
@@ -177,6 +180,47 @@ public sealed class AdvTextHistory
|
||||
layout.OriginY = y;
|
||||
}
|
||||
|
||||
public void SetWaitIndicatorObjectHandle(int requestedSlot, long handle)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
GetOrCreateLayout(slot).WaitIndicatorObjectHandle = handle;
|
||||
}
|
||||
|
||||
public void SetTextObjectRange(int requestedSlot, long firstHandle, long count)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
layout.TextObjectRangeFirst = firstHandle;
|
||||
layout.TextObjectRangeCount = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
||||
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
||||
/// teardown removes the corresponding detached presentation atomically.
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> LayoutsCoveredByTextObjectErase(long firstHandle, long count)
|
||||
{
|
||||
long eraseCount = count > 1 ? count : 1;
|
||||
var slots = new List<int>();
|
||||
foreach (var pair in _layouts)
|
||||
{
|
||||
var layout = pair.Value;
|
||||
if (RangeCovers(firstHandle, eraseCount,
|
||||
layout.TextObjectRangeFirst, layout.TextObjectRangeCount))
|
||||
slots.Add(pair.Key);
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static bool RangeCovers(long first, long count, long targetFirst, long targetCount)
|
||||
{
|
||||
if (count <= 0 || targetCount <= 0 || targetFirst < first) return false;
|
||||
ulong offset = (ulong)(targetFirst - first);
|
||||
ulong extent = (ulong)count;
|
||||
return offset <= extent && (ulong)targetCount <= extent - offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advance to the next horizontal text line. Native resets x to the layout's configured reset cursor
|
||||
/// and adds the primary font's pixel height plus the current extra line spacing to y.
|
||||
|
||||
@@ -115,7 +115,6 @@ public sealed class GfxState
|
||||
// does not erase state that the port has not modeled yet; known fields are patched by the codec.
|
||||
public byte[]? NativePersistenceRecord;
|
||||
public (long X, long Y, long Z) V18, V24, V16c;
|
||||
public long Field64, Field68, Field6c;
|
||||
public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB
|
||||
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
|
||||
public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity
|
||||
@@ -127,9 +126,10 @@ public sealed class GfxState
|
||||
// opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome
|
||||
// fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity).
|
||||
public bool OneShotColorBlend;
|
||||
// FIELD applies static mode-0 alpha after binding a looping unit sprite to suppress the retained
|
||||
// idle object while a separate clone moves between tiles. A later draw-texture rebind clears it.
|
||||
public bool StaticLoopColorBlend;
|
||||
// A mode-0 static color written after draw-texture binds an object consumes packed alpha as
|
||||
// opacity. FIELD uses this to suppress a bound idle unit, and CALLBACK_SETTING uses it when
|
||||
// rebuilding the ADV backing after CONFIG. A later draw-texture rebind clears the latch.
|
||||
public bool PostBindStaticColorBlend;
|
||||
|
||||
// ---- src-rect / spritesheet-cell channel (ops 0x239 one-shot cell, 0x231 looping animation).
|
||||
// Native obj+0x238 is the TOTAL FRAME COUNT and +0x23c is the COLUMN COUNT. Each frame keeps
|
||||
@@ -218,7 +218,7 @@ public sealed class GfxState
|
||||
|
||||
public GfxObject GetOrCreate(long handle)
|
||||
{
|
||||
// Locked: called from the VM thread (directly by 0x217/0x219/0x1ff/0x212/0x213 and inside BindDraw/anim
|
||||
// Locked: called from the VM thread (directly by geometry/animation ops and inside BindDraw/anim
|
||||
// ops) while the main-thread compositor enumerates _objects in SnapshotVisibleObjects. _lock is re-entrant
|
||||
// (Monitor) so the callers that already hold it are fine.
|
||||
lock (_lock)
|
||||
@@ -250,21 +250,6 @@ public sealed class GfxState
|
||||
lock (_lock) GetOrCreate(handle).V24 = position;
|
||||
}
|
||||
|
||||
public void SetObjectField64(long handle, long value)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).Field64 = value;
|
||||
}
|
||||
|
||||
public void SetObjectFields68And6c(long handle, long value68, long value6c)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.Field68 = value68;
|
||||
o.Field6c = value6c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x229: reset the embedded range transform, select [first, first+count), and set its
|
||||
/// anchor/pivot. This does not create or mutate an ordinary retained object.</summary>
|
||||
public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor)
|
||||
@@ -482,11 +467,10 @@ public sealed class GfxState
|
||||
{
|
||||
NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(),
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
||||
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
||||
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
|
||||
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
|
||||
OneShotColorBlend = s.OneShotColorBlend, StaticLoopColorBlend = s.StaticLoopColorBlend,
|
||||
OneShotColorBlend = s.OneShotColorBlend, PostBindStaticColorBlend = s.PostBindStaticColorBlend,
|
||||
SrcFrameCount = s.SrcFrameCount, SrcColumns = s.SrcColumns, SrcCell = s.SrcCell,
|
||||
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
|
||||
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
|
||||
@@ -637,7 +621,7 @@ public sealed class GfxState
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.StaticColorMode = mode;
|
||||
o.StaticLoopColorBlend = mode == 0 && o.Visible && o.SrcAnim;
|
||||
o.PostBindStaticColorBlend = mode == 0 && o.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,7 +895,7 @@ public sealed class GfxState
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true;
|
||||
o.StaticLoopColorBlend = false;
|
||||
o.PostBindStaticColorBlend = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,11 +1251,11 @@ public sealed class GfxState
|
||||
{
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 0 && o.StaticLoopColorBlend)
|
||||
else if (o.StaticColorMode == 0 && o.PostBindStaticColorBlend)
|
||||
{
|
||||
// FIELD clones the visible idle unit to its moving handle, then writes alpha zero
|
||||
// to the still-bound looping original. Consume that post-bind write as opacity;
|
||||
// ADV's ordinary static alpha-zero CG initialization remains the opaque mode-0 path.
|
||||
// FIELD suppresses a still-bound idle unit while its clone moves; CALLBACK_SETTING
|
||||
// rebuilds the erased ADV backing and writes the configured opacity after binding it.
|
||||
// Pre-bind static alpha-zero CG initialization remains the opaque mode-0 path.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 2)
|
||||
|
||||
@@ -16,6 +16,8 @@ public static class AdvTextLayoutBootstrap
|
||||
int? resetOpcode = table.ByLabel("reset-adv-text-layout");
|
||||
int? resetCursorOpcode = table.ByLabel("set-adv-text-reset-cursor");
|
||||
int? boundsOpcode = table.ByLabel("set-adv-text-bounds");
|
||||
int? waitIndicatorHandleOpcode = table.ByLabel("set-adv-wait-indicator-handle");
|
||||
int? textObjectRangeOpcode = table.ByLabel("set-adv-text-object-range");
|
||||
int? addOpcode = table.ByLabel("add");
|
||||
if (defineOpcode == null || resetOpcode == null) return 0;
|
||||
|
||||
@@ -60,6 +62,7 @@ public static class AdvTextLayoutBootstrap
|
||||
bool inLayoutBlock = false;
|
||||
int cursorConfigurations = 0;
|
||||
int boundConfigurations = 0;
|
||||
int objectRangeConfigurations = 0;
|
||||
foreach (var instruction in systemScript.Instructions)
|
||||
{
|
||||
if (!inLayoutBlock)
|
||||
@@ -89,20 +92,38 @@ public static class AdvTextLayoutBootstrap
|
||||
history.SetBounds(values[0], values[1], values[2]);
|
||||
boundConfigurations++;
|
||||
}
|
||||
else if (waitIndicatorHandleOpcode != null
|
||||
&& instruction.Opcode == waitIndicatorHandleOpcode.Value)
|
||||
{
|
||||
var values = ResolveConfiguration(
|
||||
instruction, localInts, systemScript.Name, expectedCount: 2);
|
||||
history.SetWaitIndicatorObjectHandle(values[0], values[1]);
|
||||
}
|
||||
else if (textObjectRangeOpcode != null
|
||||
&& instruction.Opcode == textObjectRangeOpcode.Value)
|
||||
{
|
||||
var values = ResolveConfiguration(instruction, localInts, systemScript.Name);
|
||||
history.SetTextObjectRange(values[0], values[1], values[2]);
|
||||
objectRangeConfigurations++;
|
||||
}
|
||||
|
||||
if (cursorConfigurations >= definitions && boundConfigurations >= definitions) break;
|
||||
if (cursorConfigurations >= definitions && boundConfigurations >= definitions
|
||||
&& (textObjectRangeOpcode == null || objectRangeConfigurations >= definitions))
|
||||
break;
|
||||
}
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static int[] ResolveConfiguration(
|
||||
Instruction instruction, IReadOnlyDictionary<int, long> localInts, string scriptName)
|
||||
Instruction instruction, IReadOnlyDictionary<int, long> localInts, string scriptName,
|
||||
int expectedCount = 3)
|
||||
{
|
||||
if (instruction.Args.Count != 3)
|
||||
if (instruction.Args.Count != expectedCount)
|
||||
throw new InvalidDataException(
|
||||
$"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration must use three operands");
|
||||
var values = new int[3];
|
||||
$"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration must use " +
|
||||
$"{expectedCount} operands");
|
||||
var values = new int[expectedCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (!TryResolveConstant(instruction.Args[i], localInts, out long value))
|
||||
|
||||
@@ -82,6 +82,7 @@ public sealed class VirtualMachine
|
||||
private volatile bool _advSkipServiceEnabled;
|
||||
private bool _advReadSkipState;
|
||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||
private int _messageWindowAlphaSetting;
|
||||
private int _messageGlyphDelayMilliseconds;
|
||||
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
|
||||
// Native EngineCtx owns 11 lazily allocated integer FIFOs at +0x55130. ATSEEK/MVSEEK use
|
||||
@@ -156,6 +157,7 @@ public sealed class VirtualMachine
|
||||
_sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory();
|
||||
_sharedProfile = sharedProfile ?? new SharedProfile();
|
||||
_nativeDatStore = nativeDatStore;
|
||||
_messageWindowAlphaSetting = host.MessageWindowAlphaSetting;
|
||||
_messageGlyphDelayMilliseconds = System.Math.Max(0, host.MessageGlyphDelayMilliseconds);
|
||||
_accumulatedPlaySeconds = _sharedProfile.AccumulatedPlaySeconds;
|
||||
_sessionStartTimestamp = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
@@ -2242,9 +2244,14 @@ public sealed class VirtualMachine
|
||||
case "u0041B540":
|
||||
TextHistory.SetLayoutOrigin((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
return pc + 1;
|
||||
case "get-message-window-alpha": // 0x131: host profile/config seam; default host currently supplies 0
|
||||
case "get-message-window-alpha": // 0x131: process-owned message:MesWinAlpha setting
|
||||
case "u00415F70":
|
||||
Write(a[0], _host.MessageWindowAlphaSetting); return pc + 1;
|
||||
Write(a[0], _messageWindowAlphaSetting); return pc + 1;
|
||||
case "set-message-window-alpha": // 0x141: paired message:MesWinAlpha configuration setter
|
||||
case "u0041FAA0":
|
||||
_messageWindowAlphaSetting = (int)Read(a[0]);
|
||||
_host.SetMessageWindowAlphaSetting(_messageWindowAlphaSetting);
|
||||
return pc + 1;
|
||||
case "get-message-glyph-delay": // 0x7f
|
||||
case "u00414C60":
|
||||
Write(a[0], _messageGlyphDelayMilliseconds); return pc + 1;
|
||||
@@ -2543,14 +2550,24 @@ public sealed class VirtualMachine
|
||||
case "u00420620": // upstream ABI label
|
||||
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
||||
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "set-gfx-field64": // 0x212 (idx)(val)
|
||||
Gfx.SetObjectField64(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "set-gfx-xy": // 0x213 (idx)(x)(y)
|
||||
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
|
||||
{
|
||||
Gfx.SetObjectFields68And6c(Read(a[0]), Read(a[1]), Read(a[2])); return pc + 1;
|
||||
TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1]));
|
||||
return pc + 1;
|
||||
}
|
||||
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
|
||||
{
|
||||
TextHistory.SetTextObjectRange((int)Read(a[0]), Read(a[1]), Read(a[2]));
|
||||
return pc + 1;
|
||||
}
|
||||
case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase retained-object range
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
{
|
||||
long first = Read(a[0]), count = Read(a[1]);
|
||||
Gfx.EraseRange(first, count);
|
||||
foreach (int layoutSlot in TextHistory.LayoutsCoveredByTextObjectErase(first, count))
|
||||
_host.ClearRenderedAdvTextLayout(layoutSlot);
|
||||
return pc + 1;
|
||||
}
|
||||
case "gfx-elem-release": // 0x1fa (surface slot)
|
||||
_host.ReleaseSurface((int)Read(a[0])); Gfx.ClearSurface((int)Read(a[0])); return pc + 1;
|
||||
case "clone-gfx-object": // 0x21d (source handle)(destination handle)
|
||||
|
||||
Reference in New Issue
Block a user