acdream/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs
Erik e6362da5c2 fix(diag): resolve the multisampled backbuffer before reading it
Every automated pixel gate and every blank-world verdict in Campaign V is
produced by FrameScreenshotController reading the default framebuffer with
glReadPixels. The window is created with the quality preset's MSAA sample
count, so that framebuffer is normally 4x multisampled -- and glReadPixels
against a multisampled read framebuffer is undefined per the GL spec. The
instrument the campaign has been using to decide "did the world render?"
rested on an operation with no specified result.

That is not a theoretical complaint. The blank-world investigation spent
several rounds unable to tell "the renderer drew nothing" apart from "the
readback did not return what the renderer drew", and it took an out-of-process
desktop grab to separate them. A gate cannot arbitrate a rendering defect
while its own read is unspecified.

So the capture resolves first: when the default framebuffer is multisampled
it blits the whole colour buffer into a single-sampled RGBA8 framebuffer with
identical rectangles and GL_NEAREST -- the defined resolve -- and reads that.
A single-sampled default framebuffer keeps the original direct read, so
non-MSAA captures stay byte-for-byte what they were. The blit disables and
restores the scissor test, because a blit is subject to it and a frame that
left a rectangle armed would otherwise resolve only part of the image; that
is the same self-contained-GL-state rule the render passes follow. The
resolve target is created and destroyed per capture -- captures are rare, and
a cache would have to track resize and context teardown for no gain.

GlGpuDevice.CaptureBackbuffer had the identical undefined read. It now routes
through the same path rather than being a second instrument to keep sound.

The IDefaultFramebufferSurface seam grows the draw binding, the sample count,
and the resolve operations, so the bind/query/blit/read/restore order stays
assertable without a GL context; two new tests pin the resolve order and the
resolve target's release on a failing read.

Gates: Release build green. App tests 3,866 passed / 3 skipped. Offline pixel
gate against fed636b9 passes at a differing fraction of 4.08e-05 against the
0.001 threshold -- which is exactly the same-commit control pair measured at
this commit, i.e. indistinguishable from ambient noise. Same-commit controls
re-measured at 17 px (fed636b9) and 23 px (here) out of 563,200; the recorded
band in plan section 5.1 widens to 15-23 px, fraction <= 4.1e-05.

Plan section 5.5.1 records what the connected investigation established: the
interleaved A/B attribution (4/5 vs 0/5, p ~ 0.024), the desktop witness
showing every depth-tested draw missing while the atmosphere clear and the
complete retained UI present, the probe evidence that the CPU dispatched
3,331 statics with no GL error, and the falsification list -- including the
ring glBufferSubData hazard, which condition 1 shipped against and did not
fix.

Section 5.5.2 records this session's second investigation, run against a
staged (never committed) V4c with log-only glGet* probes, and it closes the
shared-3-D-state hypothesis. The depth plane is bit-identical on blank and
rendered frames -- test on, write mask on, GL_LESS, clear value 1.0, range
[0,1], full viewport, full colour mask, no clip distances. The camera
constants are sane and advancing. Forcing gl_ClipDistance off left the blank
rate unchanged at 3/5. glGetGraphicsResetStatus returned NO_ERROR in all
1,814 samples across four blank runs, which also retires the "GPU-side fault"
reading in its context-reset form.

Two sharper facts replace it. Replacing only the frame clear colour with
magenta makes a blank frame come back uniformly magenta under the complete
retained UI, so no 3-D fragment is rasterized at all -- the world is not
drawn-then-hidden, fogged, or overdrawn. And on a blank run the client's own
capture of framebuffer 0 is RGBA(0,0,0,0) in every pixel, including pixels
where the UI is visibly on screen at that moment. That survives this commit's
resolve fix, so it is a second, independent instrument fault: the screenshot-
byte verdict used by the repeat and A/B gates measures the readback, not the
renderer, and those gates need to assert on the desktop witness instead.

V4c is NOT re-landed. No fix was attempted, because the mechanism is not
renderer state and does not sit in V4c's surface as this hypothesis predicted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 00:54:59 +02:00

622 lines
23 KiB
C#

using System.Text.Json;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Runtime;
using AcDream.Runtime.World;
using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics;
public sealed class WorldLifecycleAutomationControllerTests
{
private static readonly RenderFrameInput FrameInput = new(1d / 60d, 1280, 720);
private static readonly RenderFrameOutcome FrameOutcome = new(
new WorldRenderFrameOutcome(4, 17, NormalWorldDrawn: true),
new PrivatePresentationFrameOutcome(
PortalViewportDrawn: false,
ScreenshotCaptured: true));
[Fact]
public void ScreenshotCapture_FlipsGlRowsAndWritesACompletePng()
{
string directory = NewDirectory();
// GL order: bottom row red/green, top row blue/white.
byte[] rgba =
[
255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 255, 255, 255, 255,
];
var logs = new List<string>();
var controller = new FrameScreenshotController(
(_, _) => rgba,
directory,
logs.Add);
try
{
Assert.True(controller.TryRequest("login_stable", out string error), error);
Assert.False(controller.IsComplete("login_stable"));
Assert.True(controller.CapturePending(2, 2));
Assert.True(controller.IsComplete("login_stable"));
string path = Path.Combine(directory, "login_stable.png");
using Image image = Image.Load(path);
Assert.Equal(2, image.Width);
Assert.Equal(2, image.Height);
Assert.Contains(logs, line => line.Contains("screenshot-complete", StringComparison.Ordinal));
byte[] flipped = FrameScreenshotController.FlipRows(rgba, 2, 2);
Assert.Equal(rgba.AsSpan(8, 8).ToArray(), flipped.AsSpan(0, 8).ToArray());
Assert.Equal(rgba.AsSpan(0, 8).ToArray(), flipped.AsSpan(8, 8).ToArray());
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void ScreenshotCapture_ReportsNoWorkAndFailedCapture()
{
string directory = NewDirectory();
var controller = new FrameScreenshotController(
(_, _) => [],
directory);
try
{
Assert.False(controller.CapturePending(0, 0));
Assert.True(controller.TryRequest("bad_frame", out string error), error);
Assert.False(controller.CapturePending(0, 0));
Assert.False(controller.IsComplete("bad_frame"));
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}
[Theory]
[InlineData("")]
[InlineData("../escape")]
[InlineData("has spaces")]
[InlineData("name.png")]
public void ArtifactNames_RejectPathsAndAmbiguousCharacters(string name)
{
Assert.False(AutomationArtifactName.TryValidate(name, out string error));
Assert.NotEmpty(error);
}
/// <summary>
/// glReadPixels reads GL_READ_FRAMEBUFFER. A capture that does not name
/// framebuffer 0 captures whichever offscreen target the previous renderer
/// left bound - the paperdoll/appraisal FBOs clear to RGBA(0,0,0,0), so the
/// artifact is a fully transparent PNG that reads as a blank-world failure
/// while the backbuffer on screen was correct.
/// </summary>
[Fact]
public void DefaultFramebufferRead_BindsFramebufferZeroAndRestoresTheCallersBinding()
{
var surface = new RecordingFramebufferSurface(
boundOnEntry: 7u,
drawBoundOnEntry: 3u,
fill: 0x42);
byte[] pixels = FrameScreenshotController.ReadDefaultFramebuffer(surface, 2, 3);
Assert.Equal(
[
"bind-read 0",
"bind-draw 0",
"samples",
"read 2x3",
"bind-read 7",
"bind-draw 3",
],
surface.Calls);
Assert.Equal(0u, surface.ReadFramebufferBindingDuringRead);
Assert.Equal(2 * 3 * 4, pixels.Length);
Assert.All(pixels, value => Assert.Equal(0x42, value));
Assert.Equal(7u, surface.ReadFramebufferBinding);
Assert.Equal(3u, surface.DrawFramebufferBinding);
}
[Fact]
public void DefaultFramebufferRead_RestoresTheCallersBindingWhenTheReadThrows()
{
var surface = new RecordingFramebufferSurface(
boundOnEntry: 9u,
drawBoundOnEntry: 0u,
fill: 0)
{
ReadFailure = new InvalidOperationException("GL_OUT_OF_MEMORY"),
};
Assert.Throws<InvalidOperationException>(
() => FrameScreenshotController.ReadDefaultFramebuffer(surface, 1, 1));
Assert.Equal(
["bind-read 0", "bind-draw 0", "samples", "read 1x1", "bind-read 9", "bind-draw 0"],
surface.Calls);
Assert.Equal(9u, surface.ReadFramebufferBinding);
}
/// <summary>
/// glReadPixels against a multisampled read framebuffer is undefined per the
/// GL spec, and the window is created with the quality preset's MSAA sample
/// count — so every pixel gate and every blank-world verdict in Campaign V
/// was reading through an unspecified operation. The capture must resolve
/// the default framebuffer into a single-sampled target first and read that.
/// </summary>
[Fact]
public void MultisampledDefaultFramebufferRead_ResolvesBeforeReading()
{
var surface = new RecordingFramebufferSurface(
boundOnEntry: 7u,
drawBoundOnEntry: 4u,
fill: 0x11)
{
Samples = 4,
};
byte[] pixels = FrameScreenshotController.ReadDefaultFramebuffer(surface, 2, 3);
Assert.Equal(
[
"bind-read 0",
"bind-draw 0",
"samples",
"create-resolve 2x3",
"bind-draw 64",
"blit 2x3",
"bind-read 64",
"read 2x3",
"delete-resolve 64",
"bind-read 7",
"bind-draw 4",
],
surface.Calls);
// The blit source must still be the multisampled default framebuffer,
// and the read source must be the resolved single-sampled one.
Assert.Equal(0u, surface.ReadFramebufferBindingDuringBlit);
Assert.Equal(64u, surface.DrawFramebufferBindingDuringBlit);
Assert.Equal(64u, surface.ReadFramebufferBindingDuringRead);
Assert.Equal(2 * 3 * 4, pixels.Length);
Assert.All(pixels, value => Assert.Equal(0x11, value));
}
[Fact]
public void MultisampledDefaultFramebufferRead_DeletesTheResolveTargetWhenTheReadThrows()
{
var surface = new RecordingFramebufferSurface(
boundOnEntry: 7u,
drawBoundOnEntry: 4u,
fill: 0)
{
Samples = 4,
ReadFailure = new InvalidOperationException("GL_OUT_OF_MEMORY"),
};
Assert.Throws<InvalidOperationException>(
() => FrameScreenshotController.ReadDefaultFramebuffer(surface, 1, 1));
Assert.Contains("delete-resolve 64", surface.Calls);
Assert.Equal(0, surface.LiveResolveTargets);
Assert.Equal(7u, surface.ReadFramebufferBinding);
Assert.Equal(4u, surface.DrawFramebufferBinding);
}
private sealed class RecordingFramebufferSurface(
uint boundOnEntry,
uint drawBoundOnEntry,
byte fill)
: FrameScreenshotController.IDefaultFramebufferSurface
{
private uint _readBinding = boundOnEntry;
private uint _drawBinding = drawBoundOnEntry;
private uint _nextResolveName = 64u;
public List<string> Calls { get; } = [];
public Exception? ReadFailure { get; init; }
public int Samples { get; init; } = 1;
public int LiveResolveTargets { get; private set; }
public uint? ReadFramebufferBindingDuringRead { get; private set; }
public uint? ReadFramebufferBindingDuringBlit { get; private set; }
public uint? DrawFramebufferBindingDuringBlit { get; private set; }
public uint ReadFramebufferBinding => _readBinding;
public uint DrawFramebufferBinding => _drawBinding;
public int DefaultFramebufferSamples
{
get
{
Calls.Add("samples");
return Samples;
}
}
public void BindReadFramebuffer(uint framebuffer)
{
_readBinding = framebuffer;
Calls.Add($"bind-read {framebuffer}");
}
public void BindDrawFramebuffer(uint framebuffer)
{
_drawBinding = framebuffer;
Calls.Add($"bind-draw {framebuffer}");
}
public uint CreateResolveTarget(int width, int height)
{
Calls.Add($"create-resolve {width}x{height}");
LiveResolveTargets++;
return _nextResolveName++;
}
public void DeleteResolveTarget(uint framebuffer)
{
Calls.Add($"delete-resolve {framebuffer}");
LiveResolveTargets--;
}
public void BlitColorNearest(int width, int height)
{
Calls.Add($"blit {width}x{height}");
ReadFramebufferBindingDuringBlit = _readBinding;
DrawFramebufferBindingDuringBlit = _drawBinding;
}
public void ReadRgba(int width, int height, byte[] destination)
{
Calls.Add($"read {width}x{height}");
ReadFramebufferBindingDuringRead = _readBinding;
if (ReadFailure is not null)
throw ReadFailure;
Array.Fill(destination, fill);
}
}
[Fact]
public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot()
{
string directory = NewDirectory();
var reveal = new RuntimePortalSnapshot(
Generation: 7,
Kind: RuntimePortalKind.Portal,
Readiness: new RuntimeDestinationReadiness(
Generation: 7,
0x8A020164u,
IsIndoor: true,
IsUnhydratable: false,
RequiredRenderRadius: 0,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true),
Materialized: true,
Completed: true,
Cancelled: false,
WorldViewportObserved: true,
WorldSimulationAvailable: true,
InvariantFailureCount: 0,
WaitCueShown: false,
PortalMaterializationCount: 3);
var resources = EmptyResources() with
{
LoadedLandblocks = 1,
WorldEntities = 42,
RenderSceneOracle = new CurrentRenderSceneOracleSnapshot(
Enabled: true,
CompletedFrameSequence: 19,
AbortedFrames: 1,
ProjectionCount: 73,
OutdoorStaticCount: 41,
CellStaticCount: 20,
DynamicCount: 12,
CellBucketCount: 4,
Digest: new RenderSceneHash128(
Low: 0x0123456789ABCDEF,
High: 0xFEDCBA9876543210),
CompletedPViewFrameSequence: 19,
AbortedPViewFrames: 0,
PViewCandidateCount: 61,
PViewDigest: new RenderSceneHash128(11, 12),
DispatcherFrameSequence: 19,
AbortedDispatcherFrames: 0,
DispatcherDrawCount: 3,
DispatcherEntityCount: 52,
DispatcherMeshRefCount: 80,
DispatcherInstanceCount: 96,
DispatcherOpaqueGroupCount: 11,
DispatcherTransparentGroupCount: 4,
DispatcherDigest: new RenderSceneHash128(13, 14),
CompletedSelectionFrameSequence: 19,
AbortedSelectionFrames: 0,
SelectionPartCount: 7,
SelectionDigest: new RenderSceneHash128(15, 16)),
RenderSceneShadow =
RenderSceneShadowComparisonSnapshot.Disabled with
{
Enabled = true,
ComparisonCount = 19,
SuccessfulComparisonCount = 19,
ComparedOracleFrameSequence = 19,
MatchedProjectionCount = 73,
},
TrackedGpuBytes = 1234,
Residency = new ResidencySnapshot(
[
new ResidencyDomainSnapshot(
ResidencyDomain.Animations,
EntryCount: 2,
OwnerCount: 0,
Charges: new ResidencyCharges(
DecodedBytes: 1024),
BudgetBytes: 4096,
Hits: 8,
Misses: 3,
Evictions: 1),
],
new ResidencyCharges(DecodedBytes: 1024)),
};
var screenshots = new FrameScreenshotController(
(_, _) => [0, 0, 0, 255],
Path.Combine(directory, "screenshots"));
var controller = new WorldLifecycleAutomationController(
() => reveal,
() => new RuntimeWorldEnvironmentOwnershipSnapshot(
IsInitialized: true,
DayGroupDefinitionCount: 4,
ActiveDayGroupCount: 1),
() => new RuntimeWorldTransitOwnershipSnapshot(
BufferedTeleportDestinationCount: 0,
PendingTeleportStartCount: 0,
ActiveTeleportCount: 0,
AcceptedTeleportDestinationCount: 0,
ActiveRevealCount: 0,
PendingDestinationReadinessCount: 0,
HostProjectionCount: 0,
PendingHostAcknowledgementCount: 0),
() => 3,
_ => resources,
screenshots,
directory);
try
{
Assert.True(controller.IsWorldReady);
Assert.True(controller.IsWorldViewportVisible);
Assert.Equal(3, controller.PortalMaterializationCount);
Assert.True(controller.TryRequestCheckpoint(
"dungeon",
out IRetailUiAutomationCheckpoint? request,
out string error), error);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Pending, request.Status);
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, request.Status);
string line = Assert.Single(File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl")));
using JsonDocument json = JsonDocument.Parse(line);
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
Assert.Equal(
4,
json.RootElement.GetProperty("environmentOwnership")
.GetProperty("dayGroupDefinitionCount").GetInt32());
Assert.Equal(
0,
json.RootElement.GetProperty("transitOwnership")
.GetProperty("pendingHostAcknowledgementCount")
.GetInt32());
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
.GetProperty("visibleLandblocks").GetInt32());
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
.GetProperty("screenshotCaptured").GetBoolean());
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
JsonElement renderSceneOracle = json.RootElement
.GetProperty("resources")
.GetProperty("renderSceneOracle");
Assert.True(renderSceneOracle.GetProperty("enabled").GetBoolean());
Assert.Equal(
19,
renderSceneOracle.GetProperty("completedFrameSequence").GetInt64());
Assert.Equal(
73,
renderSceneOracle.GetProperty("projectionCount").GetInt32());
Assert.Equal(
0x0123456789ABCDEFuL,
renderSceneOracle.GetProperty("digest").GetProperty("low")
.GetUInt64());
Assert.Equal(
0xFEDCBA9876543210uL,
renderSceneOracle.GetProperty("digest").GetProperty("high")
.GetUInt64());
JsonElement renderSceneShadow = json.RootElement
.GetProperty("resources")
.GetProperty("renderSceneShadow");
Assert.True(renderSceneShadow.GetProperty("enabled").GetBoolean());
Assert.Equal(
19,
renderSceneShadow.GetProperty("comparisonCount").GetInt64());
Assert.Equal(
73,
renderSceneShadow.GetProperty("matchedProjectionCount")
.GetInt32());
Assert.Equal(
0,
renderSceneShadow.GetProperty("mismatchCount").GetInt64());
JsonElement residency = json.RootElement
.GetProperty("resources")
.GetProperty("residency");
Assert.Equal(
4096,
residency.GetProperty("totalBudgetBytes").GetInt64());
Assert.Equal(8, residency.GetProperty("totalHits").GetInt64());
Assert.Equal(
1024,
residency.GetProperty("totalCharges")
.GetProperty("decodedBytes")
.GetInt64());
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointRequests_DrainInFifoOrderOnce()
{
string directory = NewDirectory();
int captures = 0;
var controller = CreateController(
directory,
_ =>
{
captures++;
return EmptyResources();
});
try
{
Assert.True(controller.TryRequestCheckpoint(
"first", out IRetailUiAutomationCheckpoint? first, out _));
Assert.True(controller.TryRequestCheckpoint(
"second", out IRetailUiAutomationCheckpoint? second, out _));
controller.Process(FrameInput, FrameOutcome);
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(1, first.Sequence);
Assert.Equal(2, second.Sequence);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, first.Status);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, second.Status);
Assert.Equal(2, captures);
string[] lines = File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl"));
Assert.Equal(2, lines.Length);
Assert.Contains("\"name\":\"first\"", lines[0], StringComparison.Ordinal);
Assert.Contains("\"name\":\"second\"", lines[1], StringComparison.Ordinal);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointWriteFailure_IsReportedOnTokenWithoutThrowingFromFrame()
{
string directory = NewDirectory();
string blockedPath = Path.Combine(directory, "not-a-directory");
File.WriteAllText(blockedPath, "occupied");
var controller = CreateController(blockedPath, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"failed", out IRetailUiAutomationCheckpoint? request, out _));
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Failed, request.Status);
Assert.Contains("checkpoint 'failed' sequence 1 failed", request.Error);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointCancellationAndShutdown_AreExplicitAndNeverWritten()
{
string directory = NewDirectory();
var controller = CreateController(directory, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"caller", out IRetailUiAutomationCheckpoint? caller, out _));
Assert.True(controller.TryRequestCheckpoint(
"shutdown", out IRetailUiAutomationCheckpoint? shutdown, out _));
Assert.NotNull(caller);
Assert.NotNull(shutdown);
controller.CancelCheckpoint(caller);
controller.Dispose();
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, caller.Status);
Assert.Contains("cancelled before render-frame capture", caller.Error);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, shutdown.Status);
Assert.Contains("cancelled during world lifecycle automation shutdown", shutdown.Error);
Assert.False(controller.TryRequestCheckpoint(
"late", out IRetailUiAutomationCheckpoint? late, out string error));
Assert.Null(late);
Assert.Contains("shutting down", error);
Assert.False(File.Exists(Path.Combine(
directory,
"world-lifecycle.checkpoints.jsonl")));
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
private static WorldLifecycleAutomationController CreateController(
string directory,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
new(
() => default,
() => default,
() => default,
() => 0,
capture,
new FrameScreenshotController((_, _) => [], directory),
directory);
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
RenderSceneShadowComparisonSnapshot.Disabled,
RenderFrameProductComparisonSnapshot.Disabled,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
0, 0, 0, 0, 0, 0, 0, default,
default, default, 0d, 0d, null);
private static string NewDirectory()
{
string path = Path.Combine(Path.GetTempPath(), "acdream-world-gate-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
}