Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
530 lines
20 KiB
C#
530 lines
20 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class GpuWorldStateVisibilityTests
|
|
{
|
|
[Fact]
|
|
public void PendingSpawnFlood_DoesNotPublishOrRebuildLoadedView()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
const int count = 10_000;
|
|
var state = new GpuWorldState();
|
|
int edges = 0;
|
|
state.LiveProjectionVisibilityChanged += (_, _) => edges++;
|
|
long commitsBefore = state.VisibilityCommitCount;
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
state.PlaceLiveEntityProjection(
|
|
landblock,
|
|
Entity((uint)(i + 1), 0x70000000u + (uint)i));
|
|
}
|
|
|
|
Assert.Empty(state.Entities);
|
|
Assert.Equal(count, state.PendingLiveEntityCount);
|
|
Assert.Equal(commitsBefore, state.VisibilityCommitCount);
|
|
Assert.Equal(0, edges);
|
|
}
|
|
|
|
Assert.Empty(state.Entities);
|
|
Assert.Equal(count, state.PendingLiveEntityCount);
|
|
Assert.Equal(commitsBefore + 1, state.VisibilityCommitCount);
|
|
Assert.Equal(0, edges);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadedSpawnFlood_AppendsInPlaceAndPublishesOneBatch()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
const int count = 10_000;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
|
IReadOnlyList<WorldEntity> residentBucket = loaded!.Entities;
|
|
int edges = 0;
|
|
state.LiveProjectionVisibilityChanged += (_, visible) =>
|
|
{
|
|
Assert.True(visible);
|
|
edges++;
|
|
};
|
|
long commitsBefore = state.VisibilityCommitCount;
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
state.PlaceLiveEntityProjection(
|
|
landblock,
|
|
Entity((uint)(i + 1), 0x71000000u + (uint)i));
|
|
}
|
|
|
|
Assert.Same(residentBucket, loaded.Entities);
|
|
Assert.Equal(count, residentBucket.Count);
|
|
Assert.Equal(count, state.Entities.Count);
|
|
Assert.Equal(0, edges);
|
|
Assert.Equal(commitsBefore, state.VisibilityCommitCount);
|
|
}
|
|
|
|
Assert.Equal(count, edges);
|
|
Assert.Equal(commitsBefore + 1, state.VisibilityCommitCount);
|
|
Assert.Equal(count, state.Entities.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void DenseSameBucketRebucketAndDelete_PreserveEveryProjectionIndex()
|
|
{
|
|
const uint sourceLandblock = 0x0101FFFFu;
|
|
const uint targetLandblock = 0x0202FFFFu;
|
|
const int count = 10_000;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var entities = new WorldEntity[count];
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
entities[i] = Entity((uint)(i + 1), 0x72000000u + (uint)i);
|
|
state.PlaceLiveEntityProjection(sourceLandblock, entities[i]);
|
|
}
|
|
}
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
state.RebucketLiveEntity(entities[i], targetLandblock);
|
|
}
|
|
|
|
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
|
Assert.Empty(source!.Entities);
|
|
Assert.Equal(count, target!.Entities.Count);
|
|
Assert.Equal(count, state.Entities.Count);
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
state.RemoveLiveEntityProjection(entities[i]);
|
|
}
|
|
|
|
Assert.Empty(target.Entities);
|
|
Assert.Empty(state.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void DenseDemotion_CompactsLiveEntriesAndKeepsTheirIndexesValid()
|
|
{
|
|
const uint sourceLandblock = 0x0101FFFFu;
|
|
const uint targetLandblock = 0x0202FFFFu;
|
|
const int liveCount = 2_000;
|
|
var state = new GpuWorldState();
|
|
var mixed = new List<WorldEntity>(liveCount * 2);
|
|
var live = new List<WorldEntity>(liveCount);
|
|
for (int i = 0; i < liveCount; i++)
|
|
{
|
|
mixed.Add(Entity(0xC1000000u + (uint)i, 0u));
|
|
WorldEntity projection = Entity((uint)(i + 1), 0x73000000u + (uint)i);
|
|
mixed.Add(projection);
|
|
live.Add(projection);
|
|
}
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceLandblock,
|
|
new LandBlock(),
|
|
mixed));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
|
|
state.RemoveEntitiesFromLandblock(sourceLandblock);
|
|
|
|
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
|
Assert.Equal(liveCount, source!.Entities.Count);
|
|
Assert.All(source.Entities, entity => Assert.NotEqual(0u, entity.ServerGuid));
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
foreach (WorldEntity entity in live)
|
|
state.RebucketLiveEntity(entity, targetLandblock);
|
|
}
|
|
|
|
Assert.Empty(source.Entities);
|
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
|
Assert.Equal(liveCount, target!.Entities.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingDemotion_CompactsMixedStaticAndLiveEntriesAndKeepsLiveIndexesValid()
|
|
{
|
|
const uint pendingLandblock = 0x0101FFFFu;
|
|
const uint targetLandblock = 0x0202FFFFu;
|
|
var state = new GpuWorldState();
|
|
WorldEntity first = Entity(1u, 0x73500001u);
|
|
WorldEntity second = Entity(2u, 0x73500002u);
|
|
WorldEntity[] mixed =
|
|
[
|
|
Entity(0xC1000001u, 0u),
|
|
first,
|
|
Entity(0xC1000002u, 0u),
|
|
second,
|
|
];
|
|
|
|
Assert.False(state.AddEntitiesToExistingLandblock(pendingLandblock, mixed));
|
|
Assert.Equal(4, state.PendingLiveEntityCount);
|
|
|
|
state.RemoveEntitiesFromLandblock(pendingLandblock);
|
|
|
|
Assert.Equal(2, state.PendingLiveEntityCount);
|
|
state.AddLandblock(new LoadedLandblock(
|
|
pendingLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
|
|
Assert.True(state.TryGetLandblock(pendingLandblock, out LoadedLandblock? pending));
|
|
Assert.Equal([first, second], pending!.Entities);
|
|
|
|
state.RebucketLiveEntity(first, targetLandblock);
|
|
state.RebucketLiveEntity(second, targetLandblock);
|
|
|
|
Assert.Empty(pending.Entities);
|
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
|
Assert.Equal(2, target!.Entities.Count);
|
|
Assert.Contains(first, target.Entities);
|
|
Assert.Contains(second, target.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnloadToPendingThenReload_PreservesProjectionIdentityAndVisibilityEdges()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity entity = Entity(1u, 0x73600001u);
|
|
state.PlaceLiveEntityProjection(landblock, entity);
|
|
var edges = new List<(uint Guid, bool Visible)>();
|
|
state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible));
|
|
|
|
state.RemoveLandblock(landblock);
|
|
|
|
Assert.Empty(state.Entities);
|
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
|
Assert.False(state.IsLiveEntityVisible(entity.ServerGuid));
|
|
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
|
|
Assert.Same(entity, Assert.Single(state.Entities));
|
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded));
|
|
Assert.Same(entity, Assert.Single(reloaded!.Entities));
|
|
Assert.True(state.IsLiveEntityVisible(entity.ServerGuid));
|
|
Assert.Equal(
|
|
[(entity.ServerGuid, false), (entity.ServerGuid, true)],
|
|
edges);
|
|
|
|
state.RemoveLiveEntityProjection(entity);
|
|
Assert.Empty(state.Entities);
|
|
Assert.Empty(reloaded.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void BatchedVisibilityObserver_SeesCommittedFlatAndResidentViews()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity entity = Entity(1u, 0x73700001u);
|
|
int callbacks = 0;
|
|
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
|
{
|
|
callbacks++;
|
|
Assert.Equal(entity.ServerGuid, guid);
|
|
Assert.True(visible);
|
|
Assert.True(state.IsLiveEntityVisible(guid));
|
|
Assert.Same(entity, Assert.Single(state.Entities));
|
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
|
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
|
};
|
|
|
|
using (state.BeginMutationBatch())
|
|
{
|
|
state.PlaceLiveEntityProjection(landblock, entity);
|
|
Assert.Equal(0, callbacks);
|
|
}
|
|
|
|
Assert.Equal(1, callbacks);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadedToLoadedRebucket_EmitsNoVisibilityPulse()
|
|
{
|
|
const uint sourceLandblock = 0x0101FFFFu;
|
|
const uint targetLandblock = 0x0202FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity entity = Entity(1u, 0x73800001u);
|
|
state.PlaceLiveEntityProjection(sourceLandblock, entity);
|
|
var edges = new List<(uint Guid, bool Visible)>();
|
|
state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible));
|
|
|
|
state.RebucketLiveEntity(entity, targetLandblock);
|
|
|
|
Assert.Empty(edges);
|
|
Assert.True(state.IsLiveEntityVisible(entity.ServerGuid));
|
|
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
|
Assert.Empty(source!.Entities);
|
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
|
Assert.Same(entity, Assert.Single(target!.Entities));
|
|
Assert.Same(entity, Assert.Single(state.Entities));
|
|
}
|
|
|
|
[Fact]
|
|
public void SameGuidOverlap_ExactRemovalPromotesSecondaryWithoutVisibilityPulse()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
const uint guid = 0x73900001u;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity first = Entity(1u, guid);
|
|
WorldEntity second = Entity(2u, guid);
|
|
state.PlaceLiveEntityProjection(landblock, first);
|
|
state.PlaceLiveEntityProjection(landblock, second);
|
|
var edges = new List<(uint Guid, bool Visible)>();
|
|
state.LiveProjectionVisibilityChanged += (edgeGuid, visible) =>
|
|
edges.Add((edgeGuid, visible));
|
|
|
|
state.RemoveLiveEntityProjection(first);
|
|
|
|
Assert.Empty(edges);
|
|
Assert.True(state.IsLiveEntityVisible(guid));
|
|
Assert.Same(second, Assert.Single(state.Entities));
|
|
|
|
state.RemoveLiveEntityProjection(guid);
|
|
|
|
Assert.Equal([(guid, false)], edges);
|
|
Assert.False(state.IsLiveEntityVisible(guid));
|
|
Assert.Empty(state.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void DuplicateDirectPlacement_IsRejectedBeforeAnyBucketMutation()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity entity = Entity(1u, 0x74000001u);
|
|
state.PlaceLiveEntityProjection(landblock, entity);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
state.PlaceLiveEntityProjection(landblock, entity));
|
|
|
|
Assert.Same(entity, Assert.Single(state.Entities));
|
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
|
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowingObserver_DoesNotStrandLaterSubscribersOrQueuedOwnerEdges()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
WorldEntity first = Entity(1u, 0x70000001u);
|
|
WorldEntity second = Entity(2u, 0x70000002u);
|
|
var state = new GpuWorldState();
|
|
state.PlaceLiveEntityProjection(landblock, first);
|
|
state.PlaceLiveEntityProjection(landblock, second);
|
|
var observed = new List<(uint Guid, bool Visible)>();
|
|
state.LiveProjectionVisibilityChanged += (_, _) =>
|
|
throw new InvalidOperationException("fixture observer failure");
|
|
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
|
observed.Add((guid, visible));
|
|
|
|
AggregateException error = Assert.Throws<AggregateException>(() =>
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())));
|
|
|
|
Assert.Equal(2, error.InnerExceptions.Count);
|
|
Assert.Equal(
|
|
[(first.ServerGuid, true), (second.ServerGuid, true)],
|
|
observed.OrderBy(edge => edge.Guid).ToArray());
|
|
Assert.True(state.IsLiveEntityVisible(first.ServerGuid));
|
|
Assert.True(state.IsLiveEntityVisible(second.ServerGuid));
|
|
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
|
Assert.Equal(2, state.Entities.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowingRuntimeObserver_DoesNotSkipOtherOwnersOrPresentationSubscribers()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
const uint cell = 0x01010001u;
|
|
var state = new GpuWorldState();
|
|
var runtime = new LiveEntityRuntime(
|
|
state,
|
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
|
WorldSession.EntitySpawn first = Spawn(0x70000011u, cell);
|
|
WorldSession.EntitySpawn second = Spawn(0x70000012u, cell);
|
|
runtime.RegisterLiveEntity(first);
|
|
runtime.RegisterLiveEntity(second);
|
|
runtime.MaterializeLiveEntity(first.Guid, cell, id => Entity(id, first.Guid));
|
|
runtime.MaterializeLiveEntity(second.Guid, cell, id => Entity(id, second.Guid));
|
|
var observed = new List<(uint Guid, bool Visible)>();
|
|
runtime.ProjectionVisibilityChanged += (_, _) =>
|
|
throw new InvalidOperationException("fixture presentation failure");
|
|
runtime.ProjectionVisibilityChanged += (record, visible) =>
|
|
observed.Add((record.ServerGuid, visible));
|
|
|
|
AggregateException error = Assert.Throws<AggregateException>(() =>
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())));
|
|
|
|
Assert.Equal(2, error.Flatten().InnerExceptions.Count);
|
|
Assert.Equal(
|
|
[(first.Guid, true), (second.Guid, true)],
|
|
observed.OrderBy(edge => edge.Guid).ToArray());
|
|
Assert.True(runtime.TryGetRecord(first.Guid, out LiveEntityRecord firstRecord));
|
|
Assert.True(runtime.TryGetRecord(second.Guid, out LiveEntityRecord secondRecord));
|
|
Assert.True(firstRecord.IsSpatiallyVisible);
|
|
Assert.True(secondRecord.IsSpatiallyVisible);
|
|
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CopyLiveEntitiesNearLandblock_UsesBoundedNeighborhoodAndSkipsStatics()
|
|
{
|
|
var centerLive = Entity(1, 0x70000001u);
|
|
var neighborLive = Entity(2, 0x70000002u);
|
|
var farLive = Entity(3, 0x70000003u);
|
|
WorldEntity[] staticEntities = Enumerable.Range(0, 20_000)
|
|
.Select(index => Entity((uint)(index + 10), 0u))
|
|
.ToArray();
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1010FFFFu,
|
|
new LandBlock(),
|
|
staticEntities.Append(centerLive).ToArray()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1110FFFFu,
|
|
new LandBlock(),
|
|
new[] { neighborLive }));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1310FFFFu,
|
|
new LandBlock(),
|
|
new[] { farLive }));
|
|
var destination = new List<KeyValuePair<uint, WorldEntity>>
|
|
{
|
|
new(0xDEADBEEFu, farLive),
|
|
};
|
|
|
|
state.CopyLiveEntitiesNearLandblock(0x10100001u, 1, destination);
|
|
|
|
Assert.Equal(2, destination.Count);
|
|
Assert.Contains(destination, pair => pair.Key == centerLive.ServerGuid);
|
|
Assert.Contains(destination, pair => pair.Key == neighborLive.ServerGuid);
|
|
Assert.DoesNotContain(destination, pair => pair.Key == farLive.ServerGuid);
|
|
Assert.DoesNotContain(destination, pair => pair.Key == 0u);
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id, uint guid) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
|
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
Position: position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: 0x02000001u,
|
|
MotionTableId: null,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: null,
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: null,
|
|
Acceleration: null,
|
|
AngularVelocity: null,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
Timestamps: timestamps);
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "visibility fixture",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: null,
|
|
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
InstanceSequence: 1,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
}
|