Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on: [push]

jobs:
build:
name: Build and Test

runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [ "8.0" ]

steps:
- uses: actions/checkout@v4

- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet-version }}

- name: Install dependencies
run: dotnet restore

- name: Test with dotnet
run: dotnet test --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}"

- name: Upload dotnet test results
uses: actions/upload-artifact@v4
with:
name: dotnet-results-${{ matrix.dotnet-version }}
path: TestResults-${{ matrix.dotnet-version }}
# Use always() to always run this step to publish test results when there are test failures
if: ${{ always() }}
8 changes: 7 additions & 1 deletion SM_Server.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrapServer.Utility", "Scra
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrapServer.Vanilla", "ScrapServer.Vanilla\ScrapServer.Vanilla.csproj", "{420F4578-DED4-473D-8524-47B59A1D312E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScrapServer.Core", "ScrapServer.Core\ScrapServer.Core.csproj", "{C8C723D4-F703-4E3A-80FB-9423F38C9BD0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrapServer.Core", "ScrapServer.Core\ScrapServer.Core.csproj", "{C8C723D4-F703-4E3A-80FB-9423F38C9BD0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrapServer.CoreTests", "ScrapServer.CoreTests\ScrapServer.CoreTests.csproj", "{3FA34FDC-926F-445B-B315-64A2872436CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -33,6 +35,10 @@ Global
{C8C723D4-F703-4E3A-80FB-9423F38C9BD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8C723D4-F703-4E3A-80FB-9423F38C9BD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8C723D4-F703-4E3A-80FB-9423F38C9BD0}.Release|Any CPU.Build.0 = Release|Any CPU
{3FA34FDC-926F-445B-B315-64A2872436CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FA34FDC-926F-445B-B315-64A2872436CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FA34FDC-926F-445B-B315-64A2872436CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FA34FDC-926F-445B-B315-64A2872436CD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
118 changes: 14 additions & 104 deletions ScrapServer.Core/NetObjs/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

namespace ScrapServer.Core.NetObjs;

public class Character
public class Character : INetObj
{
private static int _idCounter = 1;
public NetObjType NetObjType => NetObjType.Character;
public ControllerType ControllerType => ControllerType.Unknown;

public int Id { get; private set; }
private static uint _idCounter = 1;

public uint Id { get; private set; }
public ulong OwnerId { get; set; }
public uint InventoryContainerId { get; set; } = 0;
public uint CarryContainerId { get; set; } = 0;
Expand Down Expand Up @@ -144,39 +147,25 @@ public void HandleMovement(PlayerMovement movement)
BodyRotation = matrixYaw;
HeadRotation = matrixYaw * matrixPitch;
}

public byte[] InitNetworkPacket(uint tick)
public void SerializeCreate(ref BitWriter writer)
{

// Packet 22 - Network Update
var stream = BitWriter.WithSharedPool();

var anglesLook = HeadRotation.ExtractRotation().ToEulerAngles();

var netObj = new Networking.Data.NetObj { UpdateType = NetworkUpdateType.Create, ObjectType = NetObjType.Character, Size = 0 };
var createUpdate = new CreateNetObj { ControllerType = ControllerType.Unknown };
var characterCreate = new CreateCharacter
new CreateCharacter
{
NetObjId = (uint)Id,
SteamId = OwnerId,
Position = Position,
CharacterUUID = Guid.Empty,
Pitch = ConvertAngleToBinary(anglesLook.X),
Yaw = ConvertAngleToBinary(anglesLook.Z),
WorldId = 1
};

netObj.Serialize(ref stream);
createUpdate.Serialize(ref stream);
characterCreate.Serialize(ref stream);
Networking.Data.NetObj.WriteSize(ref stream, 0);

var streamPos = stream.ByteIndex;
}.Serialize(ref writer);
}

netObj = new Networking.Data.NetObj { UpdateType = NetworkUpdateType.Update, ObjectType = NetObjType.Character, Size = 0 };
var updateCharacter = new UpdateCharacter
public void SerializeUpdate(ref BitWriter writer)
{
new UpdateCharacter
{
NetObjId = (uint)Id,
Color = new Color4(1, 1, 1, 1),
Movement = new MovementState
{
Expand All @@ -189,86 +178,7 @@ public byte[] InitNetworkPacket(uint tick)
},
SelectedItem = new Item { Uuid = Guid.Empty, InstanceId = -1 },
PlayerInfo = new PlayerId { IsPlayer = true, UnitId = PlayerId }
};

stream.GoToNearestByte();
netObj.Serialize(ref stream);
updateCharacter.Serialize(ref stream);
Networking.Data.NetObj.WriteSize(ref stream, streamPos);

var data = stream.Data.ToArray();

stream.Dispose();

return data;
}

public BlobData BlobData(uint tick)
{
var playerData = new PlayerData
{
CharacterID = Id,
SteamID = OwnerId,
InventoryContainerID = InventoryContainerId,
CarryContainer = CarryContainerId,
CarryColor = uint.MaxValue,
PlayerID = (byte)(PlayerId-1),
Name = Name,
CharacterCustomization = Customization,
};

return new BlobData
{
Uid = Guid.Parse("51868883-d2d2-4953-9135-1ab0bdc2a47e"),
Key = BitConverter.GetBytes((uint)PlayerId),
WorldID = 65534,
Flags = 13,
Data = playerData.ToBytes()
};
}

public BlobData BlobDataNeg(uint tick)
{
var playerData = new PlayerData
{
CharacterID = -1,
SteamID = OwnerId,
InventoryContainerID = InventoryContainerId,
CarryContainer = CarryContainerId,
CarryColor = uint.MaxValue,
PlayerID = (byte)(PlayerId - 1),
Name = Name,
CharacterCustomization = Customization,
};

return new BlobData
{
Uid = Guid.Parse("51868883-d2d2-4953-9135-1ab0bdc2a47e"),
Key = BitConverter.GetBytes((uint)PlayerId),
WorldID = 65534,
Flags = 13,
Data = playerData.ToBytes()
};
}

public void SpawnPackets(Player player, uint tick)
{
// Packet 13 - Generic Init Data
player.Send(new GenericInitData { Data = [BlobData(tick)], GameTick = tick });

// Packet 22 - Network Update
player.Send(new NetworkUpdate { GameTick = tick + 1, Updates = InitNetworkPacket(tick) });
}

public void RemovePackets(Player player, uint tick)
{
var netObj = new RemoveNetObj
{
Header = new Networking.Data.NetObj { UpdateType = NetworkUpdateType.Remove, ObjectType = NetObjType.Character, Size = 0 },
NetObjId = (uint)Id
};

player.Send(new NetworkUpdate { GameTick = tick, Updates = netObj.ToBytes() });
}.Serialize(ref writer);
}

public class Builder
Expand Down
189 changes: 189 additions & 0 deletions ScrapServer.Core/NetObjs/Container.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using ScrapServer.Networking;
using ScrapServer.Networking.Data;
using ScrapServer.Utility.Serialization;

namespace ScrapServer.Core.NetObjs;

public class Container : INetObj
{
public NetObjType NetObjType => NetObjType.Container;
public ControllerType ControllerType => ControllerType.Unknown;

public uint Id { get; private set; }
public readonly ushort MaximumStackSize;
public readonly ItemStack[] Items;
public readonly ISet<Guid> Filter = new HashSet<Guid>();


public record ItemStack(Guid Uuid, uint InstanceId, ushort Quantity)
{
public const uint NoInstanceId = uint.MaxValue;

public static ItemStack Empty => new(Uuid: Guid.Empty, InstanceId: NoInstanceId, Quantity: 0);

public bool IsEmpty => Uuid == Guid.Empty || Quantity == 0;

public bool IsStackableWith(ItemStack other)
{
if (other == null) return false;

if (this.IsEmpty || other.IsEmpty) return true;

return this.Uuid == other.Uuid && this.InstanceId == other.InstanceId;
}

public static ItemStack Combine(ItemStack a, ItemStack b)
{
if (!a.IsStackableWith(b))
{
throw new InvalidOperationException("Cannot add two ItemStacks that are not stackable");
}

var quantity = a.Quantity + b.Quantity;

if (quantity > ushort.MaxValue)
{
throw new InvalidOperationException("Cannot add two ItemStacks that would overflow the quantity");
}

var baseItemStack = !a.IsEmpty ? a : b;

return baseItemStack with { Quantity = (ushort)quantity };
}
}

public Container(uint id, ushort size, ushort maximumStackSize = ushort.MaxValue)
{
this.Id = id;
this.MaximumStackSize = maximumStackSize;
this.Items = Enumerable.Repeat(ItemStack.Empty, size).ToArray();
}

public Container Clone()
{
var clone = new Container(this.Id, (ushort)Items.Length, this.MaximumStackSize);
for (int i = 0; i < this.Items.Length; i++)
{
clone.Items[i] = this.Items[i];
}

clone.Filter.UnionWith(this.Filter);

return clone;
}

/// <summary>
/// Lazily find all slots containing an item with the given UUID.
/// </summary>
/// <remarks>
/// Supports mutating the container while iterating.
/// Slots behind the cursor will not be checked, while slots ahead of the cursor will be checked.
/// </remarks>
/// <param name="uuid">The UUID to search for</param>
/// <returns>An enumerable of slots containing the item</returns>
public IEnumerable<(ushort Slot, ItemStack item)> FindAllSlotsWithUuid(Guid uuid)
{
for (ushort i = 0; i < this.Items.Length; i++)
{
if (this.Items[i].Uuid == uuid)
{
yield return (i, this.Items[i]);
}
}
}

/// <summary>
/// Lazily find all empty slots in the container.
/// </summary>
/// <remarks>
/// Supports mutating the container while iterating.
/// Slots behind the cursor will not be checked, while slots ahead of the cursor will be checked.
/// </remarks>
/// <returns>An enumerable of empty slots</returns>
public IEnumerable<ushort> FindAllEmptySlots()
{
for (ushort i = 0; i < this.Items.Length; i++)
{
if (this.Items[i].IsEmpty)
{
yield return i;
}
}
}

public void SerializeCreate(ref BitWriter writer)
{
new CreateContainer
{
StackSize = this.MaximumStackSize,
Items = this.Items.Select(item => new CreateContainer.ItemStack
{
Uuid = item.Uuid,
InstanceId = item.InstanceId,
Quantity = item.Quantity
}).ToArray(),
Filter = [.. this.Filter],
}.Serialize(ref writer);
}

/// <summary>
/// Serialize the update of the container with no changes.
/// </summary>
/// <param name="writer"><inheritdoc/></param>
public void SerializeUpdate(ref BitWriter writer)
{
new UpdateContainer
{
SlotChanges = [],
Filters = [],
}.Serialize(ref writer);
}

/// <summary>
/// Create an update of the container based on the old state.
/// </summary>
/// <param name="oldState">The cloned old state of the container</param>
/// <returns>The update of the container</returns>
/// <exception cref="InvalidOperationException"></exception>
public UpdateContainer CreateNetworkUpdate(Container oldState)
{
ArgumentNullException.ThrowIfNull(oldState);

if (this.Id != oldState.Id)
{
throw new InvalidOperationException("Cannot serialize update with different container ids");
}

if (this.Items.Length != oldState.Items.Length)
{
throw new InvalidOperationException("Cannot serialize update with different item counts");
}

var slotChanges = new List<UpdateContainer.SlotChange>();

for (int i = 0; i < this.Items.Length; i++)
{
var item = this.Items[i];
var oldItem = oldState.Items[i];

if (item != oldItem)
{
slotChanges.Add(new UpdateContainer.SlotChange
{
Uuid = item.Uuid,
InstanceId = item.InstanceId,
Quantity = item.Quantity,
Slot = (ushort)i,
});
}
}

Guid[] filterChanges = this.Filter.Equals(oldState.Filter) ? [] : [.. this.Filter];

return new UpdateContainer
{
SlotChanges = [.. slotChanges],
Filters = filterChanges,
};
}
}
Loading