Bundle Project Layout¶
A bundle project is a solution with one project per compiled blob, a library for code shared between them, and a small set of convention folders. The layout below is the reference convention used by BlackICE's example bundles, nothing enforces it beyond the manifest rules, but it keeps project names aligned with blob names.
my-bundle/
├── sv/ # server component project -> sv.wasm
│ ├── sv.csproj # references replay.blackice.server
│ └── App.cs # server-side entry point
├── cl/ # client component project -> cl.wasm
│ ├── cl.csproj # references replay.blackice.client
│ └── App.cs # client-side entry point
├── sh/ # shared code, compiled into both blobs
│ ├── sh.csproj # references replay.blackice.shared
│ └── *.cs
├── data/
│ └── config.toml # the bundle manifest
├── ui/ # optional: web UI content for [ReUi]
├── vfs/ # optional: files mounted into the server sandbox
├── dist/ # staging: assembled bundle (see below)
└── Directory.Build.targets # optional: stages dist/ after every build
Components and the entry point¶
The sv and cl projects are class libraries that target wasi-wasm
through the package props and produce the two blobs. Each contains its own
top-level App class:
public static class App
{
public static void Start()
{
// Server side: register gamemodes, spawn logic, server commands.
Console.WriteLine("Hello world");
}
}
public static class App
{
public static void Start()
{
// Client side: register input handlers, HUD logic, client commands.
Console.WriteLine("Hello world");
}
}
The script host (BlackICE or REServer) calls App.Start() when the bundle is activated in that
realm:
- On the server when the bundle starts
- On each client once it has entered a certain loading stage during the connecting phase
Info
Certain operations can only be done during execution of the App.Start() phase.
This includes registering console commands, event handlers and hooks on the client side
Generally bundles divide logic like this:
- Gamemode rules, spawning, and anything authoritative go into
sv - Per-player input, presentation, and UI state goes into in
cl - Message types that cross the client/server boundary, plus helpers both
sides need, live in
sh
Shared code¶
sh is an class library referenced by both sv and cl:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="replay.blackice.shared" Version="1.0.*" />
</ItemGroup>
</Project>
Because of its dependency on replay.blackice.shared, it has access to types
like Vector3, Vector4, Quaternion, re.ClientId, re.EntityId and more
When a server or client component is compiled, the shared code it references is linked into
that component's blob. Anything in sh that a side does not use is trimmed
away.
Blob naming¶
The blob file name is the project's assembly name. Project sv produces
sv.wasm, project cl produces cl.wasm. The example manifest refers to the blobs
by those exact file names:
Keep project names short and matching, or set <AssemblyName> explicitly
and use that name in the manifest.
Assembling the bundle¶
A build produces one blob per component under
BlackICE example bundles use a Directory.Build.targets file to stage all outputs and data files in a dist/ directory.
Potential data files include the manifest config.toml, UI files and VFS content for the server side.
An example Directory.Build.targets file:
<Project>
<PropertyGroup>
<DistDir>$(MSBuildThisFileDirectory)dist\</DistDir>
</PropertyGroup>
<Target Name="CopyWasiArtifacts" AfterTargets="Build">
<MakeDir Directories="$(DistDir)" />
<Copy SourceFiles="$(MSBuildThisFileDirectory)data\config.toml"
DestinationFolder="$(DistDir)"
SkipUnchangedFiles="true"
Condition="Exists('$(MSBuildThisFileDirectory)data\config.toml')" />
<Copy SourceFiles="$(MSBuildThisFileDirectory)sv\bin\$(Configuration)\net10.0\wasi-wasm\native\sv.wasm"
DestinationFolder="$(DistDir)"
Condition="Exists('$(MSBuildThisFileDirectory)sv\bin\$(Configuration)\net10.0\wasi-wasm\native\sv.wasm')" />
<Copy SourceFiles="$(MSBuildThisFileDirectory)cl\bin\$(Configuration)\net10.0\wasi-wasm\native\cl.wasm"
DestinationFolder="$(DistDir)"
Condition="Exists('$(MSBuildThisFileDirectory)cl\bin\$(Configuration)\net10.0\wasi-wasm\native\cl.wasm')" />
</Target>
</Project>
MSBuild accepts backslashes as path separators on all platforms.
The staged dist/ folder is the actual bundle. A server owner deploys its
contents into the server's bundle directory, where the folder name becomes
the bundle name. See Bundle Config for the manifest
itself and Bundle Distribution for how a bundle is
installed and served.
Tip
A clean setup can be achieved by sym-linking the dist/ folder into
the server installations data/ugc/<bundle_name> folder. This provides a clean separation
between your source code and the produced artifacts
Example: ln -s /opt/my-mono-repo/bundle1/dist /opt/server/data/ugc/bundle1