
Security News
ECMAScript 2025 Finalized with Iterator Helpers, Set Methods, RegExp.escape, and More
ECMAScript 2025 introduces Iterator Helpers, Set methods, JSON modules, and more in its latest spec update approved by Ecma in June 2025.
A set of interfaces and a JS binding for the Screeps Arena and Screeps World APIs
A toolset and API to build bots for Screeps Arena and Screeps World using .Net 8.0.
Screeps DotNet allows you to write bots for Screeps in any language that targets .Net 7.0, for example C#, and provides tooling to compile your bot to wasm ready to be deployed to the Screeps environment.
A managed API is provided that handles the interop with the Screeps javascript API, meaning you only need to write code against a set of generic interfaces. For some examples, please see the example Arena project which contains example solutions for all 10 tutorials of Screeps Arena, or the example World project which contains a barebones Screeps World bot.
To get started making your first bot for Screeps in C#, follow these steps. You'll need a working dotnet environment as we're using terminal commands here. If you're using Visual Studio, you can use the Package Manager Console to run them.
Install the experimental wasi workload if you haven't done already.
dotnet workload install wasi-experimental
Create a new wasm project.
dotnet new wasiconsole
Edit the csproj to contain the following property groups:
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<InvariantGlobalization>true</InvariantGlobalization>
<WasmSingleFileBundle>true</WasmSingleFileBundle>
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>
<PropertyGroup>
<ScreepsCompressWasm>false</ScreepsCompressWasm>
<ScreepsEncoding>b64</ScreepsEncoding>
</PropertyGroup>
Note that the trimming, compression and encoding settings here have implications, you may need to do some research into these and play around to get settings that work for you.
Add nuget references to the following packages:
dotnet add (MyProjectName) package ScreepsDotNet.API
dotnet add (MyProjectName) package ScreepsDotNet.Bundler
Replace your Program.cs
with the following code:
using System;
using System.Diagnostics.CodeAnalysis;
using ScreepsDotNet.API.Arena;
namespace ScreepsDotNet
{
public static partial class Program
{
private static IGame? game;
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(Program))]
public static void Main()
{
// Keep the entrypoint platform independent and let Init (which is called from js) create the game instance
// This keeps the door open for unit testing later down the line
}
[System.Runtime.Versioning.SupportedOSPlatform("wasi")]
public static void Init()
{
try
{
game = new Native.Arena.NativeGame();
// TODO: Add startup logic here!
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
[System.Runtime.Versioning.SupportedOSPlatform("wasi")]
public static void Loop()
{
if (game == null) { return; }
try
{
game.Tick();
// TODO: Add loop logic here!
Console.WriteLine($"Hello world from C#, the current tick is {game.Utils.GetTicks()}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Notice the use of the DynamicDependency
and the SupportedOSPlatform
attributes on the entrypoint methods.
The DynamicDependency
attribute informs the IL trimmer that the Init
and Loop
methods are used and should not be removed.
The SupportedOSPlatform
attribute doesn't explicitly do something but will cause a warning if you accidentally try and call the method outside of wasm, for example in a unit test.
Do not change the namespace of the entrypoint as the native calls used to look it up cannot be configured to use a different namespace. You can use any namespace you like for code other than the entrypoint.
Replace your Program.cs
with the following code:
using System;
using System.Diagnostics.CodeAnalysis;
using ScreepsDotNet.API.World;
namespace ScreepsDotNet
{
public static partial class Program
{
private static IGame? game;
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(Program))]
public static void Main()
{
// Keep the entrypoint platform independent and let Init (which is called from js) create the game instance
// This keeps the door open for unit testing later down the line
}
[System.Runtime.Versioning.SupportedOSPlatform("wasi")]
public static void Init()
{
try
{
game = new Native.World.NativeGame();
// TODO: Add startup logic here!
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
[System.Runtime.Versioning.SupportedOSPlatform("wasi")]
public static void Loop()
{
if (game == null) { return; }
try
{
game.Tick();
// TODO: Add loop logic here!
Console.WriteLine($"Hello world from C#, the current tick is {game.Time}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Notice the use of the DynamicDependency
and the SupportedOSPlatform
attributes on the entrypoint methods.
The DynamicDependency
attribute informs the IL trimmer that the Init
and Loop
methods are used and should not be removed.
The SupportedOSPlatform
attribute doesn't explicitly do something but will cause a warning if you accidentally try and call the method outside of wasm, for example in a unit test.
Do not change the namespace of the entrypoint as the native calls used to look it up cannot be configured to use a different namespace. You can use any namespace you like for code other than the entrypoint.
Build the project in publish mode.
dotnet publish -c Debug
-- or --
dotnet publish -c Release
A standard build will not suffice as it does not optimise the assembly size and the generated bundle will be way too big. Debug builds normally create larger bundle sizes than release builds but should still fall within the 5mb script size limit. Release builds do take quite a bit longer to build though, so Debug builds are recommended for quick iteration.
The first ever build using ScreepsDotNet 2.x will take much longer as it has to download the binaryen sdk in order to make use of wasm-opt. This is a one-off cost per version of the bundler tool.
The build artifacts can be found at MyProjectName/bin/(Debug|Release)/net8.0/wasi-wasm/AppBundle/(arena|world)
. All will need to be copied to your Screeps environment to work properly, however generally only the bundle file will change between builds.
bootloader.d.ts
- type definitions for the bootloader. Not strictly needed, but helpful when making modifications to main.mjs
. Does not change between builds.bootloader.mjs
- js for initialising and running the dotnet runtime. Does not change between builds.bundle.mjs
- contains compressed and encoded dotnet wasm and assemblies. Changes every build, as it contains your code.main.mjs
- entrypoint for the bot. You can customise this if you want to, for example, add custom js functions that you want to call from C#.bootloader.js
- js for initialising and running the dotnet runtime. Does not change between builds but is different between Debug and Release builds.ScreepsDotNet.wasm
- contains dotnet wasm and assemblies. Changes every build, as it contains your code.main.js
- entrypoint for the bot. You can customise this if you want to, for example, add custom js functions that you want to call from C#.If all has gone well, you should now have a working basic bot that runs successfully in your Screeps environment. Note that the simulator is not supported - if you have issues with the simulator, try deploying your code to either the mmo or an up-to-date private server.
You can architect your bot however you like. Generally it is recommended to keep the Program.cs
as a slim bootstrap entrypoint and have it instantiate another class which will run the bot. You could use inversion of control to pass the instance of IGame
to your code to keep it nice and separated from the specifics of the JS interop (which also makes it much easier to mock during unit testing), but the choice really is yours. Let your imagination run wild!
If you have an existing project on .Net 7 using ScreepsDotNet 1.x, migrating to .Net 8 using ScreepsDotNet 2.x should be fairly easy. The following migration checklist should cover all needed changes for migration.
wasi-experimental
installed, as per the quickstart guide (ScreepsDotNet 1.x used a different workload)csproj
file to <Project Sdk="Microsoft.NET.Sdk">
, the TargetFramework
property to net8.0
and the RuntimeIdentifier
property to wasi-wasm
csproj
to match those listed in the quickstart guide, there may be some old ones to be removed or new ones to be addedScreepsDotNet.Bundler
and ScreepsDotNet.API
to target 2.0.0
(you might need to restart VS after the new dependencies are restored as it likes to cache the bundler build task)internal
to public
) and the namespace (must be ScreepsDotNet
)game.Tick()
in your program's Loop
function before running any of your own loop logic.The API also contains some minor breaking changes but they should only need to be addressed if they create compiler errors (for example, some cases of properties changing from int
to int?
to properly represent when the JS API may return null). Also of note is a change to the meaning of the X
and Y
properties in RoomCoord
, so take care if you've serialised these properties to memory.
The Screeps .Net API has been designed to be as close to the JS API as possible, with only minor alterations to adopt standard C# idioms. If you're familiar with the JS API, you will automatically be familiar with the .Net API.
The API is exposed as a set of interfaces and a few support structures and is contained within the ScreepsDotNet.API
namespace. Any part of the API common to both Arena and World lives directly in this namespace. Anything more specific lives in either ScreepsDotNet.API.Arena
or ScreepsDotNet.API.World
.
There is currently only one exposed concrete implementation. For Arena this is ScreepsDotNet.Native.Arena.NativeGame
implementing IGame
and for World this is ScreepsDotNet.Native.World.NativeGame
implementing IGame
. At the start of your program you can instantiate this directly. You should avoid creating multiple instances of this throughout the lifetime of your program, and instead just reuse the same instance. Note that Arena and World have very different APIs so you won't be able to write code that targets both, unless you wrap the APIs in your own layer or do alot of switching.
All other objects follow the same inheritance hierarchy as the JS API. For example - IStructureTower : IOwnedStructure : IStructure : IGameObject : IPosition
.
More details and documentation for the API is planned.
Position
of a game object, which is a struct encapsulating both X and Y. Positions can also be constructed in your own code, including from tuples, e.g. Position myPos = (30, 40);
Position
and an IPosition
, to reflect that you can use a game object in the place of a position in the JS API. In some places you may need to convert an IPosition
to a Position
by using gameObject.Position
where accepting an IPosition
is impractical in the API.IGameObject.Exists
to check that a reference is still valid. If you try to use an object that no longer exists, it will throw a NativeObjectNoLongerExists
exception. If an object starts existing again (e.g. a room or object that regains visibility), you can safely reuse the same instance again.IGameObject
as the key of a Dictionary
or safely store it in any other collection like HashSet
. Don't forget to clean up the collection when the game object is destroyed.gameObject.SetUserData<T>(T instance)
and other sibling user data methods. Only reference types can be stored in this way, and the generic type parameter itself is used as the key. You should consider user data stored in this manner to be ephemeral, e.g. it might go away at any time, so always handle the case where user data is not set. You can store as many instances of different types as you like on a game object but only one instance per type. User data lookups are more efficient than a dictionary lookup.There may be times when you want to include some custom JS in your distribution, for example some additional interop code or including a third party library. This can be achieved by adding the code to your project as JS files and including them via one (or more) of the following properties in your csproj file:
ScreepsWorldJsFiles
- Adds JS files to the output distribution, e.g. AppBundle/world
ScreepsWorldStartup
- Adds the code contained in the JS files to the main.js
before the loopScreepsWorldLoop
- Adds the code contained in the JS files to the main.js
during the loopScreepsArenaJsFiles
- Adds JS files to the output distribution, e.g. AppBundle/arena
ScreepsArenaStartup
- Adds the code contained in the JS files to the main.mjs
before the loopScreepsArenaLoop
- Adds the code contained in the JS files to the main.mjs
during the loopThe API itself uses these properties to include the bootloader and startup logic in the distribution. An example of usage can be found here.
It is possible to write some of your bot in native C and call into it from C# via icalls. Native C compiles directly to wasm whereas C# compiles to CIL which is executed by the Mono IL interpreter. The Mono IL interpreter is generally fast enough for most workloads you'll end up running in Screeps but for some compute-heavy algorithms such as pathfinding, mincut or distance transforms, every instruction counts. The following guide will demonstrate how to write a compute-heavy algorithm in native C and call it from C#.
Add a C file to your project. The name of the file and location within the project tree does not matter, but it should have the .c
extension.
For the purposes of this guide we'll add two simple methods that sum either two values or n
values. Add the following code:
#include <mono/metadata/loader.h>
int AddTwo(int a, int b)
{
return a + b;
}
int Sum(int* values, int n)
{
int result = 0;
for (int i = 0; i < n; i++)
{
result += values[i];
}
return result;
}
__attribute__((export_name("myproject_initnative")))
void myproject_initnative()
{
mono_add_internal_call("MyProject_Native::AddTwo", AddTwo);
mono_add_internal_call("MyProject_Native::Sum", Sum);
}
Add the following item group to your .csproj
file:
<ItemGroup>
<_WasmRuntimePackSrcFile Include="$(MSBuildThisFileDirectory)MyNativeCFile.c" />
<UpToDateCheckInput Include="MyNativeCFile.c" />
<ScreepsCustomInitExportNames Include="myproject_initnative" />
</ItemGroup>
Add a C# file to your project to bind the native code. It should contain the following code:
using System.Runtime.CompilerServices;
internal static class MyProject_Native
{
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern int AddTwo(int a, int b);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe int Sum(int* values, int n);
}
Note that this binding class must not be contained in any namespace and must be both internal and static. The name and signature of the icalls must match that of the native C functions exactly.
Call the native method somewhere:
Console.WriteLine($"1 + 2 = {AddTwo(1, 2)}!");
Span<int> values = [1, 2, 3];
unsafe
{
fixed (int* valuesPtr = values)
{
Console.WriteLine($"1 + 2 + 3 = {Sum(valuesPtr, values.Length)}!");
}
}
Note that any code with dependencies on a native method will only run within a wasm environment and will no longer work when being unit tested. You can solve this by encapsulating the icalls in an api and providing both managed and native implementations of your code, switching as needed. For example:
using System.Runtime.InteropServices;
public void AddTwo(int a, int b)
{
if (RuntimeInformation.OSArchitecture == Architecture.Wasm)
{
return MyProject_Native.AddTwo(a, b);
}
else
{
return a + b;
}
}
public void Sum(ReadOnlySpan<int> values)
{
if (RuntimeInformation.OSArchitecture == Architecture.Wasm)
{
unsafe
{
fixed (int* valuesPtr = values)
{
return MyProject_Native.Sum(valuesPtr, values.Length);
}
}
}
else
{
int result = 0;
foreach (int value in values)
{
result += value;
}
return result;
}
}
All the usual rules of using unsafe code apply when you're passing pointers to managed data to native code. All the safety barriers are lifted and there's alot you can do wrong to very badly break the runtime. Don't be afraid to use assert
liberally to check everything until you're confident your native code is working properly.
Screeps DotNet is made up of the following pieces:
ScreepsEncoding
property in the csproj:
<PropertyGroup>
<ScreepsEncoding>b64</ScreepsEncoding>
</PropertyGroup>
Possible encodings are bin
, b64
and b32768
.ScreepsCompressWasm
property in the csproj:
<PropertyGroup>
<ScreepsCompressWasm>true</ScreepsEncoding>
</PropertyGroup>
IGame.Utils.GetCpuTime()
or World's IGame.Cpu.GetUsed()
) throughout your main loop and early-out if it's getting too close to the 50ms/500ms limit.ScreepsDotNet is still very young and you're likely to run into all sorts of problems, including ones nobody has ever had before. Unfortunately this means this section is very small and not likely to be too useful. Still, if you're having trouble, here are some things you can try.
console.log
are ignored during the startup phase. The bootloader deals with this by storing all logs in a buffer and printing them all during the next loop instead. If you need to log something during startup, you'll need to implement something similar.Init
or the first Loop
.If you wish to use latest changes that have not yet been released to NuGet, you will need to build the project locally.
Licensed under the MIT license.
FAQs
A set of interfaces and a JS binding for the Screeps Arena and Screeps World APIs
We found that screepsdotnet.api demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ECMAScript 2025 introduces Iterator Helpers, Set methods, JSON modules, and more in its latest spec update approved by Ecma in June 2025.
Security News
A new Node.js homepage button linking to paid support for EOL versions has sparked a heated discussion among contributors and the wider community.
Research
North Korean threat actors linked to the Contagious Interview campaign return with 35 new malicious npm packages using a stealthy multi-stage malware loader.