115 lines
4.0 KiB
C#
115 lines
4.0 KiB
C#
namespace Ananta.Server.Protocol.Client4229938;
|
||
|
||
/// <summary>
|
||
/// 搜打撤的格子占位模型 —— 逐条镜像客户端 LX6/Manager/ExtractionShooterUtils.lua 的
|
||
/// OccupyCells / CheckCanAddItemByCell / TryGetFreeCellByOrder。
|
||
///
|
||
/// 为什么必须镜像:客户端拖拽落地前会**自己**用这套规则校验一次(CheckShiftItemInBag、
|
||
/// CheckCellAllowed、TryGetFreeCellIndexByConfigId),服务端如果只判"原点格空不空",
|
||
/// 就会出现「2x3 的物品和 1 格的物品叠在一起」——因为 2x3 的覆盖区里那些非原点格在服务端看来是空的。
|
||
///
|
||
/// 三条硬约束(照抄客户端,不要"优化"):
|
||
/// 1. 坐标是 0 基,合法范围 x∈[0,sizeX-1]、y∈[0,sizeY-1]。
|
||
/// 2. 物品占 Volume.x × Volume.y,IsRotated 时两者互换。
|
||
/// 3. 找空格是 y 外层、x 内层(先铺满一行再换行),无旋转优先、旋转次之。
|
||
/// </summary>
|
||
internal sealed class ExtractionGrid4229938
|
||
{
|
||
private readonly bool[] _cells;
|
||
|
||
internal ExtractionGrid4229938(int sizeX, int sizeY)
|
||
{
|
||
SizeX = Math.Max(1, sizeX);
|
||
SizeY = Math.Max(1, sizeY);
|
||
_cells = new bool[SizeX * SizeY];
|
||
}
|
||
|
||
internal int SizeX { get; }
|
||
internal int SizeY { get; }
|
||
|
||
/// <summary>把 Volume 按旋转换算成实际占格 (宽, 高)。</summary>
|
||
internal static (int W, int H) Rotate(int volumeX, int volumeY, bool isRotated)
|
||
=> isRotated ? (Math.Max(1, volumeY), Math.Max(1, volumeX))
|
||
: (Math.Max(1, volumeX), Math.Max(1, volumeY));
|
||
|
||
internal bool InBounds(int x, int y)
|
||
=> x >= 0 && y >= 0 && x < SizeX && y < SizeY;
|
||
|
||
internal bool IsFree(int x, int y) => InBounds(x, y) && !_cells[(y * SizeX) + x];
|
||
|
||
/// <summary>整块 (x,y,w,h) 是否都在界内且没被占。对应客户端 CheckCanAddItemByCell。</summary>
|
||
internal bool CanPlace(int x, int y, int w, int h)
|
||
{
|
||
if (x < 0 || y < 0 || w <= 0 || h <= 0)
|
||
return false;
|
||
if (x + w > SizeX || y + h > SizeY)
|
||
return false;
|
||
|
||
for (var oy = 0; oy < h; oy++)
|
||
for (var ox = 0; ox < w; ox++)
|
||
if (_cells[((y + oy) * SizeX) + x + ox])
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
internal void Occupy(int x, int y, int w, int h)
|
||
{
|
||
for (var oy = 0; oy < h; oy++)
|
||
for (var ox = 0; ox < w; ox++)
|
||
if (InBounds(x + ox, y + oy))
|
||
_cells[((y + oy) * SizeX) + x + ox] = true;
|
||
}
|
||
|
||
internal void Release(int x, int y, int w, int h)
|
||
{
|
||
for (var oy = 0; oy < h; oy++)
|
||
for (var ox = 0; ox < w; ox++)
|
||
if (InBounds(x + ox, y + oy))
|
||
_cells[((y + oy) * SizeX) + x + ox] = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按客户端的顺序找第一个能放下 (w,h) 的格子:y 外层、x 内层。
|
||
/// 与 TryGetFreeCellByOrder 的扫描顺序一致 —— 顺序不一致会导致服务端认为"放得下"、
|
||
/// 客户端认为"放不下",玩家看到的就是拖过去弹回。
|
||
/// </summary>
|
||
internal bool TryFirstFit(int w, int h, out int x, out int y)
|
||
{
|
||
x = 0;
|
||
y = 0;
|
||
if (w > SizeX || h > SizeY)
|
||
return false;
|
||
|
||
for (var cy = 0; cy <= SizeY - h; cy++)
|
||
for (var cx = 0; cx <= SizeX - w; cx++)
|
||
if (CanPlace(cx, cy, w, h))
|
||
{
|
||
x = cx;
|
||
y = cy;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>先试不旋转,再试旋转。对应 TryGetFreeCellByOrderWithRotation。</summary>
|
||
internal bool TryFirstFitWithRotation(int volumeX, int volumeY, out int x, out int y, out bool rotated)
|
||
{
|
||
if (TryFirstFit(volumeX, volumeY, out x, out y))
|
||
{
|
||
rotated = false;
|
||
return true;
|
||
}
|
||
|
||
if (TryFirstFit(volumeY, volumeX, out x, out y))
|
||
{
|
||
rotated = true;
|
||
return true;
|
||
}
|
||
|
||
rotated = false;
|
||
return false;
|
||
}
|
||
}
|