14 Commits
89 changed files with 5509 additions and 23645 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "##build "
prefix-development: "##build "
labels:
- buildsystem
+80
View File
@@ -11,10 +11,90 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
outputs:
is_release: ${{ steps.release.outputs.is_release }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Build Docker image
run: make build
- name: Check for a release tag
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
id: release
shell: bash
run: |
tag="$(git describe --exact-match --tags "$GITHUB_SHA" 2>/dev/null || true)"
if [[ "$tag" =~ ^[0-9] ]]; then
echo "is_release=yes" >> "$GITHUB_OUTPUT"
echo "tag_name=$tag" >> "$GITHUB_OUTPUT"
else
echo "is_release=no" >> "$GITHUB_OUTPUT"
fi
- name: Check release version
if: ${{ steps.release.outputs.is_release == 'yes' }}
env:
TAG_NAME: ${{ steps.release.outputs.tag_name }}
run: |
version=$(./version.sh)
if [ "$TAG_NAME" != "$version" ]; then
echo "Tag $TAG_NAME does not match version $version" >&2
exit 1
fi
- name: Create distribution archive
if: ${{ steps.release.outputs.is_release == 'yes' }}
env:
TAG_NAME: ${{ steps.release.outputs.tag_name }}
run: |
archive="r2wars-$TAG_NAME"
container=$(docker create r2wars:latest)
trap 'docker rm -f "$container" >/dev/null' EXIT
docker cp "$container:/r2wars/." release-bin
mkdir "$archive"
cp -R release-bin/. "$archive/"
cp -R examples "$archive/warriors"
cp RELEASE_README.md "$archive/README.txt"
zip -r "$archive.zip" "$archive"
- name: Store distribution archive
if: ${{ steps.release.outputs.is_release == 'yes' }}
uses: actions/upload-artifact@v7
with:
name: r2wars-${{ steps.release.outputs.tag_name }}
path: r2wars-${{ steps.release.outputs.tag_name }}.zip
if-no-files-found: error
release:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.build.outputs.is_release == 'yes' }}
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download distribution archive
uses: actions/download-artifact@v8
with:
name: r2wars-${{ needs.build.outputs.tag_name }}
path: release
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ needs.build.outputs.tag_name }}
run: |
archive="release/r2wars-$TAG_NAME.zip"
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
gh release upload "$TAG_NAME" "$archive" --clobber
else
gh release create "$TAG_NAME" "$archive" --verify-tag --generate-notes
fi
+39
View File
@@ -0,0 +1,39 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: "1 4 * * 4"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript, csharp ]
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: none
queries: +security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
+43
View File
@@ -0,0 +1,43 @@
name: WebAssembly
on:
push:
paths:
- ".github/workflows/wasm.yml"
- "csharp/wwwroot/**"
- "wasm/**"
- "Makefile"
pull_request:
paths:
- ".github/workflows/wasm.yml"
- "csharp/wwwroot/**"
- "wasm/**"
- "Makefile"
permissions:
contents: read
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: wasm/package-lock.json
- run: npm ci
working-directory: wasm
- run: npm test
working-directory: wasm
- run: npm run build
working-directory: wasm
- name: Check shared .NET client bundle
run: git diff --exit-code -- csharp/wwwroot/assets/r2wars-ui.js
- uses: actions/upload-artifact@v7
with:
name: r2wars-wasm
path: wasm/dist
if-no-files-found: error
retention-days: 7
+2
View File
@@ -0,0 +1,2 @@
csharp/bin/
csharp/obj/
+36 -76
View File
@@ -1,106 +1,66 @@
# syntax=docker/dockerfile:labs
ARG UBUNTU_RELEASE="22.04"
# syntax=docker/dockerfile:1
ARG UBUNTU_RELEASE="24.04"
ARG TIMEZONE="UTC"
ARG R2_SOURCE="https://github.com/radareorg/radare2.git"
ARG R2_INSTALL_DIR="/opt/r2"
ARG CCACHE_DIR="/root/.cache/ccache"
ARG CC="ccache gcc"
ARG CXX="ccache g++"
ARG DOTNET_IMAGE="10.0-noble"
ARG R2_VERSION="6.2.0"
################################################################################
# Base image for builder/runner #
# Download the pinned radare2 release package #
################################################################################
FROM ubuntu:${UBUNTU_RELEASE} as base
ARG TIMEZONE
FROM ubuntu:${UBUNTU_RELEASE} AS r2-package
ARG R2_VERSION
ARG TARGETARCH
# Prepare timezone settings
ENV TZ=${TIMEZONE}
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
echo ${TIMEZONE} > /etc/timezone
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl
# Enable APT package caching
RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN case "${TARGETARCH}" in \
amd64) R2_SHA256="eb82324e83315887fbee6f5d8632c982c593e056a87180f1bec5ccb06c463aeb" ;; \
arm64) R2_SHA256="e866525e9874588d478d536cca38cf9a7562896725efb4119b886101fd93f1ec" ;; \
*) echo "Unsupported Docker architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac && \
curl --fail --location --show-error \
"https://github.com/radareorg/radare2/releases/download/${R2_VERSION}/radare2_${R2_VERSION}_${TARGETARCH}.deb" \
--output /radare2.deb && \
echo "${R2_SHA256} /radare2.deb" | sha256sum --check --strict
################################################################################
# r2wars builder #
################################################################################
FROM base as r2wars-builder
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_IMAGE} AS r2wars-builder
# Install base packages
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends \
mono-complete
# Add r2wars source and build it
COPY --link csharp /r2wars
WORKDIR /r2wars
RUN xbuild /p:Configuration=Release r2wars.csproj
RUN dotnet publish r2wars.csproj \
--configuration Release \
--output /r2wars-publish \
--no-self-contained
################################################################################
# r2 builder #
################################################################################
FROM base as r2-builder
ARG CCACHE_DIR
ARG CC
ARG CXX
ARG R2_SOURCE
ARG R2_INSTALL_DIR
# Install base packages
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
ccache \
cmake \
ninja-build \
pkg-config \
git \
ca-certificates \
python3-pip && \
python3 -m pip install --no-cache-dir 'meson>=0.63'
# By default, add r2 source from GitHub and build it -- replace the git repo
# with your local path if you want to build your custom radare2 source tree
ADD ${R2_SOURCE} /r2src
WORKDIR /r2src
RUN --mount=type=cache,id=ccache,target=${CCACHE_DIR},sharing=shared \
meson setup build \
-Dbuildtype=release \
-Dprefix=${R2_INSTALL_DIR} \
-Dlocal=true && \
ninja -C build install
################################################################################
# r2wars runner #
################################################################################
FROM base as runner
ARG R2_INSTALL_DIR
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_IMAGE} AS runner
ARG TIMEZONE
ENV TZ=${TIMEZONE}
# Install base packages
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=bind,from=r2-package,source=/radare2.deb,target=/tmp/radare2.deb \
apt-get update && \
apt-get install -y --no-install-recommends \
mono-runtime \
nuget
apt-get install -y --no-install-recommends /tmp/radare2.deb
# Copy r2 and r2wars in from the build stages
COPY --from=r2wars-builder --link /r2wars/bin/Release /r2wars
COPY --from=r2-builder --link ${R2_INSTALL_DIR} ${R2_INSTALL_DIR}
ENV PATH=${PATH}:${R2_INSTALL_DIR}/bin
COPY --from=r2wars-builder --link /r2wars-publish /r2wars
EXPOSE 9664 9966
WORKDIR /r2wars
ENTRYPOINT ["mono", "r2wars.exe"]
ENTRYPOINT ["dotnet", "r2wars.dll"]
+15 -1
View File
@@ -1,6 +1,7 @@
DOCKER ?= docker
WASM_ARCHIVE ?= r2wars-wasm.zip
.PHONY: default build start stop clean
.PHONY: default build start stop clean wasm-build wasm-test wasm-run wasm-dist
default: start ## By default, just start the container
@@ -22,3 +23,16 @@ clean: stop ## Remove the project containers and docker image
@if $(DOCKER) image inspect r2wars:latest >/dev/null 2>&1; then \
$(DOCKER) image rm r2wars:latest; \
fi
wasm-build: ## Build the standalone browser/WebAssembly version
cd wasm && npm install && npm run build
wasm-test: ## Test the browser engine against radare2 WebAssembly
cd wasm && npm install && npm test
wasm-run: ## Run the standalone browser/WebAssembly development server
cd wasm && npm install && npm run dev
wasm-dist: wasm-build ## Build a static-host-ready WebAssembly zip archive
cd wasm/dist && zip -qrFS "../$(WASM_ARCHIVE)" .
@echo "Created wasm/$(WASM_ARCHIVE)"
+173 -27
View File
@@ -1,46 +1,192 @@
# r2wars
![Alt text](csharp/resources/r2wars_logo_transparent.png?raw=true "r2wars Logo")
![r2wars logo](csharp/resources/r2wars_logo_transparent.png?raw=true "r2wars")
The C# implementation of the r2wars tournament competition
r2wars is a Core War-style tournament powered by radare2 and ESIL. Two
assembly warriors share a 1 KiB arena and take turns executing instructions.
Each warrior tries to corrupt its opponent until the opponent crashes, traps,
or leaves the arena.
Tournament api from: https://github.com/otac0n/tournaments
Warriors may target different CPU architectures while competing in the same
address space.
## Description
## Choose how to run it
r2wars is a game similar to corewars, where 2 programs run on a
shared memory space trying to catch each other in order to trash
their code and make them crash.
| Mode | Requirements | Execution | Best for |
| --- | --- | --- | --- |
| Docker | Docker with Compose | .NET and native radare2 in containers | Fastest local setup |
| Native | .NET 10 and radare2 | Local .NET server and native radare2 | Development and full architecture support |
| WebAssembly | Node.js/npm for building | Entirely inside the browser | Static hosting and serverless deployment |
This game was initially developed by pancake as a PoC in here
The WebAssembly implementation is an alternative execution path. It does not
replace or change the existing .NET and Docker flows.
* https://github.com/radare/radare2-extras/tree/master/r2wars
## Quick start with Docker
You can refer to the r2wars For N00bs talk from r2con2020 for more information:
From the repository root:
* https://www.youtube.com/watch?v=PB0AFBqFwGQ
```sh
make start
```
Furthermore, you can find an explanation of the game in the first competition
that happened during the 2nd r2con in 2017.
Open <http://127.0.0.1:9664/>. The local `warriors/` directory is mounted
read-only into the container.
* https://www.youtube.com/watch?v=sB-i5yUatx4
Useful commands:
This repository contains an evolved implementation of the engine
written in C# by SkUaTeR dropping the MFC requirement that was
making it impossible to run outside Windows.
```sh
make build # build the Docker image
make start # start r2wars and its nginx frontend
make stop # stop the containers
make clean # stop and remove the r2wars image
```
The solution was to use an embedded webserver that provides a
web interface using websockets to stream the process changes
from the M
The Docker image pins radare2 6.2.0 and verifies the official `amd64` or
`arm64` release package before installing it.
## Dependencies
## Run natively
* Mono / .NET Runtime (always from mono project some distros packages dont work)
* radare2
Install the .NET 10 SDK and radare2, then run:
On windows you need to have radare2.exe and rasm2.exe in the same
directory as the r2wars.exe executable.
```sh
dotnet run --project csharp/r2wars.csproj -- ../warriors
```
On Mac/Linux/BSD, r2wars will try to find them in the PATH.
Open <http://127.0.0.1:9664/>.
--pancake
On Linux, macOS, and BSD, `radare2` and `rasm2` are loaded from `PATH`. On
Windows, put `radare2.exe` and `rasm2.exe` in `PATH` or alongside the published
r2wars application. The native interface uses the same bot manager and editor
as the WebAssembly build; live previews are assembled by native `rasm2`.
## WebAssembly version
The browser version runs the tournament controller and radare2 ESIL locally in
a Web Worker. It has no Kestrel or WebSocket connection, does not require
Docker or .NET at runtime, and never uploads warrior source or tournament state.
### Build and run locally
```sh
make wasm-build
make wasm-run
```
Then open <http://127.0.0.1:5173/>. The production site is generated in
`wasm/dist/`.
The first build downloads the official radare2 6.2.0 WASI API module and
verifies the checksums of both the release archive and extracted Wasm module.
The module is about 44 MB before HTTP or ZIP compression.
### Create a deployable ZIP
```sh
make wasm-dist
```
This creates `wasm/r2wars-wasm.zip`. The archive contains the complete static
site with `index.html` at its root:
```text
index.html
radare2.wasm
assets/
warriors/
```
Extract those files directly into a static server's document root. No
application backend, .NET runtime, radare2 installation, or WebSocket proxy is
needed. The paths are relative, so the site can also be hosted below a URL
prefix.
The server should deliver `.wasm` files as `application/wasm`. Opening
`index.html` through `file://` is not supported because browsers restrict local
Wasm and module loading; use any ordinary HTTP(S) static server instead.
You can choose a different archive name when needed:
```sh
make wasm-dist WASM_ARCHIVE=my-r2wars-build.zip
```
### Shared web controls
- The WebAssembly build initially loads three bundled bots. The .NET build
loads the configured warriors directory.
- Open **Bots** while the tournament is not running to inspect the complete
roster, create or import bots, rename them, edit their assembly, or remove
them.
- Bot edits are drafts. They do not modify a running or paused tournament and
are validated and loaded only when **Start new tournament** is selected.
- Every filename or source edit is assembled immediately by the selected
engine—radare2 Wasm in the static build or native `rasm2` in the .NET build.
The editor shows a red or green result, the compiled size, and the complete
byte array; results over the 512-byte warrior limit are marked red.
- Select **Choose warriors** to replace the draft roster with local `.asm`
files. Dropping `.asm` files onto the page adds or updates bots by filename.
- Tournaments support run, pause, single-cycle stepping, bounded history,
standings, and downloadable reports.
- In the WebAssembly build, tournament execution stays in a worker so the
interface remains responsive.
### Browser architecture support
The official radare2 6.2.0 WASI module contains x86, ARM, MIPS, RISC-V, and
Game Boy support. It does not contain the 8051 plugin, so 8051 warriors require
the native or Docker version for now.
See [wasm/README.md](wasm/README.md) for implementation details and direct npm
commands.
## Writing warriors
A tournament requires at least two non-empty `.asm` files. The architecture
and bitness are encoded before the `.asm` extension:
```text
scanner.x86-32.asm
hammer.arm-64.asm
probe.mips-32.asm
```
Supported filename markers are:
- `.x86-32.asm` and `.x86-64.asm`
- `.arm-16.asm`, `.arm-32.asm`, and `.arm-64.asm`
- `.mips-32.asm` and `.mips-64.asm`
- `.riscv-32.asm` and `.riscv-64.asm`
- `.gb.asm`
- `.8051.asm` in the native and Docker versions
Examples for several architectures are available in `examples/`. The default
native and Docker tournament reads warriors from `warriors/`.
## WebAssembly development commands
Run these from the repository root:
```sh
make wasm-test # test parsers and the engine against the real radare2 Wasm
make wasm-build # type-check and build wasm/dist/
make wasm-run # start the Vite development server
make wasm-dist # build and package wasm/r2wars-wasm.zip
```
The dedicated WebAssembly CI workflow runs the tests and production build, then
uploads `wasm/dist/` as the `r2wars-wasm` artifact.
## Background
The original proof of concept lives in
[radare2-extras](https://github.com/radare/radare2-extras/tree/master/r2wars).
This repository evolved the engine into a portable C# implementation by
SkUaTeR, replacing the original Windows-only MFC interface with a web UI.
Further introductions:
- [r2wars for N00bs — r2con 2020](https://www.youtube.com/watch?v=PB0AFBqFwGQ)
- [r2wars competition — r2con 2017](https://www.youtube.com/watch?v=sB-i5yUatx4)
- [Tournament library](https://github.com/otac0n/tournaments)
— pancake
+2 -2
View File
@@ -12,8 +12,8 @@ supporting other architectures with ESIL as emulation engine.
Introduction
------------
In order to start the competition you need to run r2wars.exe
which is written in C# and runs on Mono, .NET/Core or MSCLR.
In order to start the competition you need the ASP.NET Core Runtime 10,
then run `dotnet r2wars.dll [warriors-directory]`.
The UI has been rewritten from the original MFC to be in HTML,
so the program will run a local webserver which serves a
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>
-76
View File
@@ -1,76 +0,0 @@
using System;
using System.IO;
using System.Reflection;
using Nancy;
using Nancy.Bootstrapper;
using Nancy.Responses;
using Nancy.Session;
using Nancy.ViewEngines;
using Nancy.TinyIoc;
namespace r2warsTorneo
{
public class CustomBootstrapper : DefaultNancyBootstrapper
{
protected override void ConfigureApplicationContainer(TinyIoCContainer container)
{
base.ConfigureApplicationContainer(container);
ResourceViewLocationProvider.RootNamespaces.Add(Assembly.GetAssembly(typeof(MainModule)), "r2warsTorneo.Views");
}
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
CookieBasedSessions.Enable(pipelines);
}
/*protected override void ConfigureConventions(NancyConventions conventions)
{
base.ConfigureConventions(conventions);
conventions.StaticContentsConventions.Add(AddStaticResourcePath("/content", Assembly.GetAssembly(typeof(MainModule)), "r2wars.Web.Views.content"));
}*/
protected override NancyInternalConfiguration InternalConfiguration
{
get
{
return NancyInternalConfiguration.WithOverrides(OnConfigurationBuilder);
}
}
void OnConfigurationBuilder(NancyInternalConfiguration x)
{
x.ViewLocationProvider = typeof(ResourceViewLocationProvider);
}
protected override byte[] FavIcon
{
get { return null; }
}
public static Func<NancyContext, string, Response> AddStaticResourcePath(string requestedPath, Assembly assembly, string namespacePrefix)
{
return (context, s) =>
{
var path = context.Request.Path;
if (!path.StartsWith(requestedPath))
{
return null;
}
string resourcePath;
string name;
var adjustedPath = path.Substring(requestedPath.Length + 1);
if (adjustedPath.IndexOf('/') >= 0)
{
name = Path.GetFileName(adjustedPath);
resourcePath = namespacePrefix + "." + adjustedPath.Substring(0, adjustedPath.Length - name.Length - 1).Replace('/', '.');
}
else
{
name = adjustedPath;
resourcePath = namespacePrefix;
}
return new EmbeddedFileResponse(assembly, resourcePath, name);
};
}
}
}
-40
View File
@@ -1,40 +0,0 @@
using Nancy;
namespace r2warsTorneo
{
public class MainModule : NancyModule
{
public MainModule()
{
Get["/"] = x => View["index.html"];
}
}
/*public class r2warsModule : NancyModule
{
public r2warsModule() : base("/r2wars")
{
Get["/refresh"] = x =>
{
//return r2wars.Codigo.r2wars.json_output();
return "";
};
Get["/next"] = x =>
{
//r2wars.Codigo.r2wars.btStep_Click();
//return r2wars.Codigo.r2wars.json_output();
return "";
};
Get["/start"] = x =>
{
//r2wars.Codigo.r2wars.btAuto_Click();
//return "";
return "";
};
Get["/reset"] = x =>
{
//r2wars.Codigo.r2wars.btInit_Click();
//return r2wars.Codigo.r2wars.json_output();
return "";
};
}
}*/
}
+9 -8
View File
@@ -1,22 +1,22 @@
VERSION=$(shell ../version.sh)
R2W=r2wars-$(VERSION)
DOTNET?=dotnet
all:
msbuild r2wars.sln
$(DOTNET) build r2wars.sln
run:
mono bin/Release/r2wars.exe
$(DOTNET) run --project r2wars.csproj -- ../warriors
ls:
$(SHELL) lsbots.sh
dist:
msbuild r2wars.sln /p:Configuration=Release
rm -rf $(R2W)
mkdir -p $(R2W)
cp -rf examples bin/Release/*.exe bin/Release/*.dll $(R2W)
cp -f RELEASE_README.md $(R2W)/README.txt
mv $(R2W)/examples $(R2W)/warriors
$(DOTNET) publish r2wars.csproj --configuration Release --output $(R2W) --no-self-contained
cp -f ../RELEASE_README.md $(R2W)/README.txt
mkdir -p $(R2W)/warriors
cp -rf ../examples/. $(R2W)/warriors
zip -r $(R2W).zip $(R2W)
rm -rf $(R2W)
@@ -24,4 +24,5 @@ r2w32:
wget -c http://radare.mikelloc.com/get/2.7.0/radare2_installer-msvc_64-2.7.0.exe
clean:
rm -rf bin obj r2wars r2wars.zip
$(DOTNET) clean r2wars.sln
rm -rf bin obj r2wars-*.zip
+56 -57
View File
@@ -1,8 +1,13 @@
using System;
using System;
using System.Diagnostics;
using System.Threading;
using System.IO;
using System.Net.WebSockets;
using System.Threading.Tasks;
using WebSocketSharp.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace r2warsTorneo
{
static public class r2warsStatic
@@ -10,67 +15,61 @@ namespace r2warsTorneo
static public r2wars r2w = new r2wars();
static public Torneo torneo = new Torneo();
}
class Program
{
static void Main(string[] args)
static async Task Main(string[] args)
{
string httpUrl = "http://0.0.0.0:9664";
string websocketUrl = "ws://0.0.0.0:9966";
string listenAddress = OperatingSystem.IsWindows() ? "127.0.0.1" : "0.0.0.0";
string httpUrl = Environment.GetEnvironmentVariable("R2WARS_HTTP_URL") ??
"http://" + listenAddress + ":9664";
string websocketUrl = Environment.GetEnvironmentVariable("R2WARS_WEBSOCKET_URL") ??
"http://" + listenAddress + ":9966";
if (args.Length > 0)
r2warsStatic.torneo.SetWarriorsDirectory(args[0]);
WebApplicationBuilder builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions
{
ContentRootPath = AppContext.BaseDirectory,
WebRootPath = Path.Combine(AppContext.BaseDirectory, "wwwroot")
});
builder.WebHost.UseKestrel();
builder.WebHost.UseUrls(httpUrl, websocketUrl);
builder.Services.AddRouting();
WebApplication app = builder.Build();
app.UseWebSockets();
app.UseStaticFiles();
app.Map("/r2wars", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("A WebSocket connection is required.");
return;
}
using WebSocket socket = await context.WebSockets.AcceptWebSocketAsync();
await r2warsWebSocket.HandleAsync(socket, context.RequestAborted);
});
app.MapFallbackToFile("index.html");
Console.WriteLine("Web server running at " + httpUrl);
Console.WriteLine("WebSocket server running at " + websocketUrl.Replace("http://", "ws://") + "/r2wars");
Console.WriteLine("Press Ctrl-C to stop r2wars.");
if (OperatingSystem.IsWindows())
{
httpUrl = "http://127.0.0.1:9664";
websocketUrl = "ws://127.0.0.1:9966";
}
var tokenSource = new CancellationTokenSource();
CancellationToken ct = tokenSource.Token;
var shutdown = new ManualResetEvent(false);
Console.CancelKeyPress += (sender, eventArgs) =>
{
eventArgs.Cancel = true;
shutdown.Set();
};
if (args.Length > 0) {
r2warsStatic.torneo.SetWarriorsDirectory(args[0]);
Process.Start(new ProcessStartInfo(httpUrl)
{
UseShellExecute = true
});
}
var taskA = new Task(() =>
{
try
{
var nancyHost = new Nancy.Hosting.Self.NancyHost(new Uri(httpUrl), new CustomBootstrapper());
nancyHost.Start();
Console.WriteLine("Web server running at " + httpUrl);
if (OperatingSystem.IsWindows())
{
Process.Start(httpUrl);
}
while (!ct.IsCancellationRequested) { Thread.Sleep(1000); }
nancyHost.Stop();
}
catch (Exception exception)
{
Console.Error.WriteLine("Failed to start web server: " + exception);
}
}, tokenSource.Token);
var taskB = new Task(() =>
{
var wssv = new WebSocketServer(websocketUrl);
wssv.AddWebSocketService<r2warsWebSocket>("/r2wars");
wssv.Start();
while (!ct.IsCancellationRequested) { Thread.Sleep(1000); }
wssv.Stop();
}, tokenSource.Token);
taskA.Start();
taskB.Start();
Console.WriteLine("Press Ctrl-C to stop r2wars.");
shutdown.WaitOne();
tokenSource.Cancel();
Task.WaitAll(new Task[] { taskA, taskB }, 5000);
await app.RunAsync();
}
}
}
-36
View File
@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// La información general de un ensamblado se controla mediante el siguiente
// conjunto de atributos. Cambie estos valores de atributo para modificar la información
// asociada con un ensamblado.
[assembly: AssemblyTitle("WindowsFormsApplication1")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("WindowsFormsApplication1")]
[assembly: AssemblyCopyright("Copyright © 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Si establece ComVisible en false, los tipos de este ensamblado no estarán visibles
// para los componentes COM. Si necesita obtener acceso a un tipo de este ensamblado desde
// COM, establezca el atributo ComVisible en true en este tipo.
[assembly: ComVisible(false)]
// El siguiente GUID sirve como id. de typelib si este proyecto se expone a COM.
[assembly: Guid("7c07ab0a-d2b5-4546-b5b2-6dc6e77e40d1")]
// La información de versión de un ensamblado consta de los cuatro valores siguientes:
//
// Versión principal
// Versión secundaria
// Número de compilación
// Revisión
//
// Puede especificar todos los valores o usar los valores predeterminados de número de compilación y de revisión
// mediante el carácter '*', como se muestra a continuación:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.4.0.0")]
[assembly: AssemblyFileVersion("0.4.0.0")]
-63
View File
@@ -1,63 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Este código fue generado por una herramienta.
// Versión de runtime:4.0.30319.42000
//
// Los cambios en este archivo podrían causar un comportamiento incorrecto y se perderán si
// se vuelve a generar el código.
// </auto-generated>
//------------------------------------------------------------------------------
namespace r2warsTorneo.Properties {
using System;
/// <summary>
/// Clase de recurso fuertemente tipado, para buscar cadenas traducidas, etc.
/// </summary>
// StronglyTypedResourceBuilder generó automáticamente esta clase
// a través de una herramienta como ResGen o Visual Studio.
// Para agregar o quitar un miembro, edite el archivo .ResX y, a continuación, vuelva a ejecutar ResGen
// con la opción /str o recompile su proyecto de VS.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Devuelve la instancia de ResourceManager almacenada en caché utilizada por esta clase.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("r2warsTorneo.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Reemplaza la propiedad CurrentUICulture del subproceso actual para todas las
/// búsquedas de recursos mediante esta clase de recurso fuertemente tipado.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
-117
View File
@@ -1,117 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
-26
View File
@@ -1,26 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Este código fue generado por una herramienta.
// Versión de runtime:4.0.30319.42000
//
// Los cambios en este archivo podrían causar un comportamiento incorrecto y se perderán si
// se vuelve a generar el código.
// </auto-generated>
//------------------------------------------------------------------------------
namespace r2warsTorneo.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}
-7
View File
@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>
+83
View File
@@ -32,6 +32,7 @@ namespace r2warsTorneo
string actualCombatLog = "";
string actualDeadReason = "";
string warriorsDirectory = "warriors";
string browserWarriorsDirectory = "";
string workflow = "idle";
string workflowMessage = "Load warriors to begin.";
string scores = "No tournament loaded.";
@@ -46,6 +47,7 @@ namespace r2warsTorneo
r2w.Event_combatEnd += new MyHandler1(CombatEnd);
r2w.Event_roundEnd += new MyHandler1(RoundEnd);
r2w.Event_roundExhausted += new MyHandler1(RoundExhausted);
AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => CleanupBrowserWarriors();
}
public void SetWarriorsDirectory(string wd)
@@ -402,6 +404,7 @@ namespace r2warsTorneo
public void LoadTournamentPlayers()
{
StopTournament();
CleanupBrowserWarriors();
string[] files;
try
{
@@ -422,6 +425,86 @@ namespace r2warsTorneo
CreatePairings(files, "mixed (detected from each filename)", ".asm");
}
public List<BrowserWarriorSource> GetWarriorSources()
{
string[] files = loadedWarriors.Where(File.Exists).ToArray();
if (files.Length == 0)
{
if (!Directory.Exists(warriorsDirectory))
return new List<BrowserWarriorSource>();
files = Directory.GetFiles(warriorsDirectory)
.Where(path => path.EndsWith(".asm", StringComparison.OrdinalIgnoreCase))
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
return files.Select(path => new BrowserWarriorSource
{
Name = Path.GetFileName(path),
Source = File.ReadAllText(path)
}).ToList();
}
public void LoadTournamentSources(IReadOnlyList<BrowserWarriorSource> warriors)
{
if (warriors == null)
throw new ArgumentNullException(nameof(warriors));
if (warriors.Count > 128)
throw new InvalidOperationException("A tournament cannot contain more than 128 bots.");
HashSet<string> names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (BrowserWarriorSource warrior in warriors)
{
string name = (warrior?.Name ?? "").Trim();
if (name.Length == 0 || name != Path.GetFileName(name) || name.Contains('/') || name.Contains('\\') ||
name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
throw new InvalidOperationException("Bot filenames must not contain a directory path.");
if (!name.EndsWith(".asm", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException(name + ": filename must end in .asm");
if (WarriorCompiler.ArchitectureFromName(name) == r2archs.eArch.unknown)
throw new InvalidOperationException(name + ": architecture must be encoded in the filename");
if (!names.Add(name))
throw new InvalidOperationException("Duplicate bot filename: " + name);
if (string.IsNullOrWhiteSpace(warrior.Source))
throw new InvalidOperationException(name + ": source is empty");
}
StopTournament();
string directory = Path.Combine(Path.GetTempPath(), "r2wars-web-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
try
{
string[] files = warriors
.OrderBy(warrior => warrior.Name, StringComparer.OrdinalIgnoreCase)
.Select(warrior =>
{
string path = Path.Combine(directory, warrior.Name.Trim());
File.WriteAllText(path, warrior.Source);
return path;
}).ToArray();
CleanupBrowserWarriors();
browserWarriorsDirectory = directory;
CreatePairings(files, "mixed (browser sources)", ".asm");
}
catch
{
try { Directory.Delete(directory, true); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
throw;
}
}
private void CleanupBrowserWarriors()
{
if (string.IsNullOrEmpty(browserWarriorsDirectory))
return;
string directory = browserWarriorsDirectory;
browserWarriorsDirectory = "";
try { Directory.Delete(directory, true); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
public void ResetTournament()
{
StopTournament();
@@ -3,7 +3,6 @@
/// <summary>
/// Thrown when an IPairingsGenerator is asked to load a state that does not represent a valid tournament within its constraints.
/// </summary>
[global::System.Serializable]
public class InvalidTournamentStateException : Exception
{
/// <summary>
@@ -32,16 +31,4 @@
: base(message, inner)
{
}
/// <summary>
/// Initializes a new instance of the InvalidTournamentStateException class with serialized data.
/// </summary>
/// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual information about the source or destination.</param>
/// <exception cref="System.ArgumentNullException">The info parameter is null.</exception>
/// <exception cref="System.Runtime.Serialization.SerializationException">The class name is null or System.Exception.HResult is zero (0).</exception>
protected InvalidTournamentStateException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
}
}
File diff suppressed because it is too large. Load diff
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace r2warsTorneo
{
public sealed class BrowserWarriorSource
{
public string Name { get; set; } = "";
public string Source { get; set; } = "";
}
public sealed class WarriorAssemblyResult
{
public bool Ok { get; set; }
public byte[] Bytes { get; set; } = Array.Empty<byte>();
public int Size { get { return Bytes.Length; } }
public string Message { get; set; } = "";
}
public static class WarriorCompiler
{
public const int MaximumWarriorSize = 512;
public static r2archs.eArch ArchitectureFromName(string name)
{
return r2archs.archfromfileext((name ?? "").ToLowerInvariant());
}
public static WarriorAssemblyResult Assemble(BrowserWarriorSource warrior)
{
if (warrior == null)
return Failure("No bot source was provided.");
r2archs.eArch architecture = ArchitectureFromName(warrior.Name);
if (architecture == r2archs.eArch.unknown)
return Failure((warrior.Name ?? "Unnamed bot") + ": architecture must be encoded in the filename");
if (string.IsNullOrWhiteSpace(warrior.Source))
return Failure((warrior.Name ?? "Unnamed bot") + ": source is empty");
if (warrior.Source.Length > 64 * 1024)
return Failure((warrior.Name ?? "Unnamed bot") + ": source is too large to preview");
string temporaryFile = Path.Combine(Path.GetTempPath(), "r2wars-assemble-" + Guid.NewGuid().ToString("N") + ".asm");
try
{
File.WriteAllText(temporaryFile, warrior.Source);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = r2paths.rasm2,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (string argument in r2archs.rasm2param(architecture).Split(' ', StringSplitOptions.RemoveEmptyEntries))
startInfo.ArgumentList.Add(argument);
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add(temporaryFile);
using Process process = Process.Start(startInfo);
if (process == null)
return Failure("rasm2 could not be started.");
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
process.WaitForExit();
string stdout = stdoutTask.GetAwaiter().GetResult();
string stderr = stderrTask.GetAwaiter().GetResult().Trim();
string hex = new string(stdout.Where(character => !char.IsWhiteSpace(character)).ToArray());
if (process.ExitCode != 0 || hex.Length == 0 || hex.Length % 2 != 0 || !hex.All(Uri.IsHexDigit))
return Failure(stderr.Length > 0 ? stderr : "rasm2 could not assemble this bot.");
byte[] bytes = new byte[hex.Length / 2];
for (int index = 0; index < bytes.Length; index++)
bytes[index] = Convert.ToByte(hex.Substring(index * 2, 2), 16);
bool oversized = bytes.Length > MaximumWarriorSize;
return new WarriorAssemblyResult
{
Ok = !oversized,
Bytes = bytes,
Message = oversized
? "Compiles, but exceeds the " + MaximumWarriorSize + "-byte warrior limit."
: "Compiles successfully."
};
}
catch (Exception exception)
{
return Failure(exception.Message);
}
finally
{
try { File.Delete(temporaryFile); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
}
private static WarriorAssemblyResult Failure(string message)
{
return new WarriorAssemblyResult { Ok = false, Message = message };
}
}
}
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Nancy" version="1.4.4" targetFramework="net452" />
<package id="Nancy.Hosting.Self" version="1.4.1" targetFramework="net452" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net452" />
<package id="WebSocketSharp" version="1.0.3-rc11" targetFramework="net452" />
</packages>
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -1,216 +0,0 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Nancy.Hosting.Self</name>
</assembly>
<members>
<member name="T:Nancy.Hosting.Self.IgnoredHeaders">
<summary>
A helper class that checks for a header against a list of headers that should be ignored
when populating the headers of an <see cref="T:System.Net.HttpListenerResponse"/> object.
</summary>
</member>
<member name="M:Nancy.Hosting.Self.IgnoredHeaders.IsIgnored(System.String)">
<summary>
Determines if a header is ignored when populating the headers of an
<see cref="T:System.Net.HttpListenerResponse"/> object.
</summary>
<param name="headerName">The name of the header.</param>
<returns><c>true</c> if the header is ignored; otherwise, <c>false</c>.</returns>
</member>
<member name="T:Nancy.Hosting.Self.UrlReservations">
<summary>
Configuration for automatic url reservation creation
</summary>
</member>
<member name="P:Nancy.Hosting.Self.UrlReservations.CreateAutomatically">
<summary>
Gets or sets a value indicating whether url reservations
are automatically created when necessary.
Defaults to false.
</summary>
</member>
<member name="P:Nancy.Hosting.Self.UrlReservations.User">
<summary>
Gets or sets a value for the user to use to create the url reservations for.
Defaults to the "Everyone" group.
</summary>
</member>
<member name="T:Nancy.Hosting.Self.UacHelper">
<summary>
Helpers for UAC
</summary>
</member>
<member name="M:Nancy.Hosting.Self.UacHelper.RunElevated(System.String,System.String)">
<summary>
Run an executable elevated
</summary>
<param name="file">File to execute</param>
<param name="args">Arguments to pass to the executable</param>
<returns>True if successful, false otherwise</returns>
</member>
<member name="T:Nancy.Hosting.Self.HostConfiguration">
<summary>
Host configuration for the self host
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.RewriteLocalhost">
<summary>
Gets or sets a property that determines if localhost uris are
rewritten to htp://+:port/ style uris to allow for listening on
all ports, but requiring either a url reservation, or admin
access
Defaults to true.
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.UrlReservations">
<summary>
Configuration around automatically creating url reservations
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.AllowChunkedEncoding">
<summary>
Gets or sets a property that determines if Transfer-Encoding: Chunked is allowed
for the response instead of Content-Length (default: true).
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.UnhandledExceptionCallback">
<summary>
Gets or sets a property that provides a callback to be called
if there's an unhandled exception in the self host.
Note: this will *not* be called for normal nancy Exceptions
that are handled by the Nancy handlers.
Defaults to writing to debug out.
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.EnableClientCertificates">
<summary>
Gets or sets a property that determines whether client certificates
are enabled or not.
When set to true the self host will request a client certificate if the
request is running over SSL.
Defaults to false.
</summary>
</member>
<member name="P:Nancy.Hosting.Self.HostConfiguration.AllowAuthorityFallback">
<summary>
Gets or sets a property determining if base uri matching can fall back to just
using Authority (Schema + Host + Port) as base uri if it cannot match anything in
the known list. This should only be set to True if you have issues with port forwarding
(e.g. on Azure).
</summary>
</member>
<member name="T:Nancy.Hosting.Self.NancyHost">
<summary>
Allows to host Nancy server inside any application - console or windows service.
</summary>
<remarks>
NancyHost uses <see cref="T:System.Net.HttpListener"/> internally. Therefore, it requires full .net 4.0 profile (not client profile)
to run. <see cref="M:Nancy.Hosting.Self.NancyHost.Start"/> will launch a thread that will listen for requests and then process them. Each request is processed in
its own execution thread. NancyHost needs <see cref="T:System.SerializableAttribute"/> in order to be used from another appdomain under
mono. Working with AppDomains is necessary if you want to unload the dependencies that come with NancyHost.
</remarks>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(System.Uri[])">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUris"/>.
Uses the default configuration
</summary>
<param name="baseUris">The <see cref="T:System.Uri"/>s that the host will listen to.</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(Nancy.Hosting.Self.HostConfiguration,System.Uri[])">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUris"/>.
Uses the specified configuration.
</summary>
<param name="baseUris">The <see cref="T:System.Uri"/>s that the host will listen to.</param>
<param name="configuration">Configuration to use</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(Nancy.Bootstrapper.INancyBootstrapper,System.Uri[])">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUris"/>, using
the provided <paramref name="bootstrapper"/>.
Uses the default configuration
</summary>
<param name="bootstrapper">The bootstrapper that should be used to handle the request.</param>
<param name="baseUris">The <see cref="T:System.Uri"/>s that the host will listen to.</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(Nancy.Bootstrapper.INancyBootstrapper,Nancy.Hosting.Self.HostConfiguration,System.Uri[])">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUris"/>, using
the provided <paramref name="bootstrapper"/>.
Uses the specified configuration.
</summary>
<param name="bootstrapper">The bootstrapper that should be used to handle the request.</param>
<param name="configuration">Configuration to use</param>
<param name="baseUris">The <see cref="T:System.Uri"/>s that the host will listen to.</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(System.Uri,Nancy.Bootstrapper.INancyBootstrapper)">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUri"/>, using
the provided <paramref name="bootstrapper"/>.
Uses the default configuration
</summary>
<param name="baseUri">The <see cref="T:System.Uri"/> that the host will listen to.</param>
<param name="bootstrapper">The bootstrapper that should be used to handle the request.</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.#ctor(System.Uri,Nancy.Bootstrapper.INancyBootstrapper,Nancy.Hosting.Self.HostConfiguration)">
<summary>
Initializes a new instance of the <see cref="T:Nancy.Hosting.Self.NancyHost"/> class for the specified <paramref name="baseUri"/>, using
the provided <paramref name="bootstrapper"/>.
Uses the specified configuration.
</summary>
<param name="baseUri">The <see cref="T:System.Uri"/> that the host will listen to.</param>
<param name="bootstrapper">The bootstrapper that should be used to handle the request.</param>
<param name="configuration">Configuration to use</param>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.Dispose">
<summary>
Stops the host if it is running.
</summary>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.Start">
<summary>
Start listening for incoming requests with the given configuration
</summary>
</member>
<member name="M:Nancy.Hosting.Self.NancyHost.Stop">
<summary>
Stop listening for incoming requests.
</summary>
</member>
<member name="T:Nancy.Hosting.Self.NetSh">
<summary>
Executes NetSh commands
</summary>
</member>
<member name="M:Nancy.Hosting.Self.NetSh.AddUrlAcl(System.String,System.String)">
<summary>
Add a url reservation
</summary>
<param name="url">Url to add</param>
<param name="user">User to add the reservation for</param>
<returns>True if successful, false otherwise.</returns>
</member>
<member name="T:Nancy.Hosting.Self.AutomaticUrlReservationCreationFailureException">
<summary>
Exception for when automatic address reservation creation fails.
Provides the user with manual instructions.
</summary>
</member>
<member name="P:Nancy.Hosting.Self.AutomaticUrlReservationCreationFailureException.Message">
<summary>
Gets a message that describes the current exception.
</summary>
<returns>
The error message that explains the reason for the exception, or an empty string("").
</returns>
<filterpriority>1</filterpriority>
</member>
<member name="T:Nancy.Hosting.Self.UriExtensions">
<summary>
Extension methods for working with <see cref="T:System.Uri"/> instances.
</summary>
</member>
</members>
</doc>
@@ -1,31 +0,0 @@
This Microsoft .NET Library may incorporate components from the projects listed
below. Microsoft licenses these components under the Microsoft .NET Library
software license terms. The original copyright notices and the licenses under
which Microsoft received such components are set forth below for informational
purposes only. Microsoft reserves all rights not expressly granted herein,
whether by implication, estoppel or otherwise.
1. .NET Core (https://github.com/dotnet/core/)
.NET Core
Copyright (c) .NET Foundation and Contributors
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,128 +0,0 @@
MICROSOFT SOFTWARE LICENSE TERMS
MICROSOFT .NET LIBRARY
These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. Please read them. They apply to the software named above, which includes the media on which you received it, if any. The terms also apply to any Microsoft
· updates,
· supplements,
· Internet-based services, and
· support services
for this software, unless other terms accompany those items. If so, those terms apply.
BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. IF YOU DO NOT ACCEPT THEM, DO NOT USE THE SOFTWARE.
IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE PERPETUAL RIGHTS BELOW.
1. INSTALLATION AND USE RIGHTS.
a. Installation and Use. You may install and use any number of copies of the software to design, develop and test your programs.
b. Third Party Programs. The software may include third party programs that Microsoft, not the third party, licenses to you under this agreement. Notices, if any, for the third party program are included for your information only.
2. ADDITIONAL LICENSING REQUIREMENTS AND/OR USE RIGHTS.
a. DISTRIBUTABLE CODE. The software is comprised of Distributable Code. “Distributable Code” is code that you are permitted to distribute in programs you develop if you comply with the terms below.
i. Right to Use and Distribute.
· You may copy and distribute the object code form of the software.
· Third Party Distribution. You may permit distributors of your programs to copy and distribute the Distributable Code as part of those programs.
ii. Distribution Requirements. For any Distributable Code you distribute, you must
· add significant primary functionality to it in your programs;
· require distributors and external end users to agree to terms that protect it at least as much as this agreement;
· display your valid copyright notice on your programs; and
· indemnify, defend, and hold harmless Microsoft from any claims, including attorneys’ fees, related to the distribution or use of your programs.
iii. Distribution Restrictions. You may not
· alter any copyright, trademark or patent notice in the Distributable Code;
· use Microsoft’s trademarks in your programs’ names or in a way that suggests your programs come from or are endorsed by Microsoft;
· include Distributable Code in malicious, deceptive or unlawful programs; or
· modify or distribute the source code of any Distributable Code so that any part of it becomes subject to an Excluded License. An Excluded License is one that requires, as a condition of use, modification or distribution, that
· the code be disclosed or distributed in source code form; or
· others have the right to modify it.
3. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not
· work around any technical limitations in the software;
· reverse engineer, decompile or disassemble the software, except and only to the extent that applicable law expressly permits, despite this limitation;
· publish the software for others to copy;
· rent, lease or lend the software;
· transfer the software or this agreement to any third party; or
· use the software for commercial software hosting services.
4. BACKUP COPY. You may make one backup copy of the software. You may use it only to reinstall the software.
5. DOCUMENTATION. Any person that has valid access to your computer or internal network may copy and use the documentation for your internal, reference purposes.
6. EXPORT RESTRICTIONS. The software is subject to United States export laws and regulations. You must comply with all domestic and international export laws and regulations that apply to the software. These laws include restrictions on destinations, end users and end use. For additional information, see www.microsoft.com/exporting.
7. SUPPORT SERVICES. Because this software is “as is,” we may not provide support services for it.
8. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services.
9. APPLICABLE LAW.
a. United States. If you acquired the software in the United States, Washington state law governs the interpretation of this agreement and applies to claims for breach of it, regardless of conflict of laws principles. The laws of the state where you live govern all other claims, including claims under state consumer protection laws, unfair competition laws, and in tort.
b. Outside the United States. If you acquired the software in any other country, the laws of that country apply.
10. LEGAL EFFECT. This agreement describes certain legal rights. You may have other rights under the laws of your country. You may also have rights with respect to the party from whom you acquired the software. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so.
11. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. YOU MAY HAVE ADDITIONAL CONSUMER RIGHTS OR STATUTORY GUARANTEES UNDER YOUR LOCAL LAWS WHICH THIS AGREEMENT CANNOT CHANGE. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
FOR AUSTRALIA – YOU HAVE STATUTORY GUARANTEES UNDER THE AUSTRALIAN CONSUMER LAW AND NOTHING IN THESE TERMS IS INTENDED TO AFFECT THOSE RIGHTS.
12. LIMITATION ON AND EXCLUSION OF REMEDIES AND DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.
This limitation applies to
· anything related to the software, services, content (including code) on third party Internet sites, or third party programs; and
· claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law.
It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages.
Please note: As this software is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French.
Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français.
EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues.
LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices.
Cette limitation concerne :
· tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et
· les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur.
Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard.
EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas.
File diff suppressed because it is too large. Load diff
-26
View File
@@ -1,34 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading.Tasks;
using System.IO;
public static class OperatingSystem
{
public static bool IsWindows()
{
string windir = Environment.GetEnvironmentVariable("windir");
return (!string.IsNullOrEmpty(windir) && windir.Contains(@"\") && Directory.Exists(windir));
}
public static bool IsMacOS()
{
return File.Exists(@"/System/Library/CoreServices/SystemVersion.plist");
}
public static bool IsLinux()
{
if (File.Exists(@"/proc/sys/kernel/ostype"))
{
string osType = File.ReadAllText(@"/proc/sys/kernel/ostype");
return osType.StartsWith("Linux", StringComparison.OrdinalIgnoreCase);
}
return false;
}
}
namespace r2warsTorneo
{
public class r2wars
+9 -111
View File
@@ -1,118 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7C07AB0A-D2B5-4546-B5B2-6DC6E77E40D1}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>r2warsTorneo</RootNamespace>
<AssemblyName>r2wars</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
<AssemblyTitle>r2wars</AssemblyTitle>
<Product>r2wars</Product>
<Version>0.4.0</Version>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Reference Include="Nancy, Version=1.4.4.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\Nancy.1.4.4\lib\net40\Nancy.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nancy.Hosting.Self, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\Nancy.Hosting.Self.1.4.1\lib\net40\Nancy.Hosting.Self.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="websocket-sharp, Version=1.0.2.59611, Culture=neutral, PublicKeyToken=5660b08a1845a91e, processorArchitecture=MSIL">
<HintPath>packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Content Update="wwwroot/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Compile Include="clsEngine.cs" />
<Compile Include="clsPipe.cs" />
<Compile Include="CustomBootstrapper.cs" />
<Compile Include="Torneo.cs" />
<Compile Include="MainModule.cs" />
<Compile Include="MyEvent.cs" />
<Compile Include="r2warsWebSocket.cs" />
<Compile Include="Torneo\InvalidTournamentStateException.cs" />
<Compile Include="Torneo\PairingsGeneratorState.cs" />
<Compile Include="Torneo\roundrobinparse.cs" />
<Compile Include="Torneo\Score.cs" />
<Compile Include="Torneo\TournamentNameTable.cs" />
<Compile Include="Torneo\TournamentPairing.cs" />
<Compile Include="Torneo\TournamentRanking.cs" />
<Compile Include="Torneo\TournamentRound.cs" />
<Compile Include="Torneo\TournamentTeam.cs" />
<Compile Include="Torneo\TournamentTeamScore.cs" />
<Compile Include="r2wars.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Views\index.html" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
+238 -87
View File
@@ -1,100 +1,251 @@
using WebSocketSharp;
using WebSocketSharp.Server;
using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace r2warsTorneo
{
public class r2warsWebSocket : WebSocketBehavior
public static class r2warsWebSocket
{
MyHandler1 h1;
protected override void OnMessage(MessageEventArgs e)
private const int MaxMessageSize = 1024 * 1024;
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
string recv = e.Data;
string msg = "";
if (recv == "cmd_state")
{
msg = r2warsStatic.torneo.GetStateJson();
}
else if (recv == "cmd_prevlog")
{
msg = r2warsStatic.r2w.prevLog();
}
else if (recv == "cmd_nextlog")
{
msg = r2warsStatic.r2w.nextLog();
}
else if (recv == "cmd_load")
{
r2warsStatic.torneo.LoadTournamentPlayers();
}
else if (recv == "cmd_reset")
{
r2warsStatic.torneo.ResetTournament();
}
else if (recv == "cmd_run")
{
r2warsStatic.torneo.RunTournamentCombats();
}
else if (recv == "cmd_stop")
{
r2warsStatic.torneo.StopActualCombat();
}
else if (recv == "cmd_step" || recv == "cmd_next")
{
r2warsStatic.torneo.StepTournamentCombats();
}
else if (recv == "cmd_dbg4")
{
r2warsStatic.r2w.bStopAtRoundStart = false;
}
else if (recv == "cmd_dbg4si")
{
r2warsStatic.r2w.bStopAtRoundStart = true;
}
else if (recv == "cmd_dbg5")
{
r2warsStatic.r2w.bStopAtRoundEnd = false;
}
else if (recv == "cmd_dbg5si")
{
r2warsStatic.r2w.bStopAtRoundEnd = true;
}
else if (recv == "moreflow")
{
r2warsStatic.r2w.sync_var = true;
msg = "none";
}
else if (recv == "arch_arm")
{
r2warsStatic.r2w.answer = "arm";
}
else if (recv == "arch_x86")
{
r2warsStatic.r2w.answer = "x86";
}
if (msg!="")
Send(msg);
}
private void R2wars_EventPinta(object sender, MyEvent e)
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static async Task HandleAsync(WebSocket socket, CancellationToken cancellationToken)
{
r2warsStatic.r2w.sync_var = false;
Send(e.message);
}
protected override void OnError(ErrorEventArgs e)
{
base.OnError(e);
Channel<string> outgoing = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false
});
MyHandler1 drawHandler = (sender, drawEvent) =>
{
r2warsStatic.r2w.sync_var = false;
outgoing.Writer.TryWrite(drawEvent.message);
};
using CancellationTokenSource connectionCancellation =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
CancellationToken connectionToken = connectionCancellation.Token;
r2warsStatic.r2w.Event_draw += drawHandler;
outgoing.Writer.TryWrite(r2warsStatic.torneo.GetStateJson());
try
{
Task receiveTask = ReceiveLoopAsync(socket, outgoing.Writer, connectionToken);
Task sendTask = SendLoopAsync(socket, outgoing.Reader, connectionToken);
await Task.WhenAny(receiveTask, sendTask);
connectionCancellation.Cancel();
outgoing.Writer.TryComplete();
try
{
await Task.WhenAll(receiveTask, sendTask);
}
catch (OperationCanceledException) when (connectionToken.IsCancellationRequested)
{
}
catch (WebSocketException)
{
// The peer disconnected without completing a close handshake.
}
}
finally
{
r2warsStatic.r2w.Event_draw -= drawHandler;
outgoing.Writer.TryComplete();
}
}
protected override void OnClose(CloseEventArgs e)
private static async Task ReceiveLoopAsync(
WebSocket socket,
ChannelWriter<string> outgoing,
CancellationToken cancellationToken)
{
if (h1 != null)
r2warsStatic.r2w.Event_draw -= h1;
base.OnClose(e);
byte[] buffer = new byte[4096];
while (socket.State == WebSocketState.Open)
{
using MemoryStream message = new MemoryStream();
WebSocketReceiveResult result;
do
{
result = await socket.ReceiveAsync(
new ArraySegment<byte>(buffer), cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseOutputAsync(
WebSocketCloseStatus.NormalClosure,
"Closing",
CancellationToken.None);
return;
}
if (result.MessageType != WebSocketMessageType.Text)
{
await socket.CloseAsync(
WebSocketCloseStatus.InvalidMessageType,
"Only text commands are supported.",
cancellationToken);
return;
}
message.Write(buffer, 0, result.Count);
if (message.Length > MaxMessageSize)
{
await socket.CloseAsync(
WebSocketCloseStatus.MessageTooBig,
"Command is too large.",
cancellationToken);
return;
}
}
while (!result.EndOfMessage);
string response = Dispatch(Encoding.UTF8.GetString(message.ToArray()));
if (!string.IsNullOrEmpty(response))
outgoing.TryWrite(response);
}
}
protected override void OnOpen()
private static async Task SendLoopAsync(
WebSocket socket,
ChannelReader<string> outgoing,
CancellationToken cancellationToken)
{
h1 = new MyHandler1(R2wars_EventPinta);
r2warsStatic.r2w.Event_draw += h1;
Send(r2warsStatic.torneo.GetStateJson());
base.OnOpen();
await foreach (string message in outgoing.ReadAllAsync(cancellationToken))
{
byte[] payload = Encoding.UTF8.GetBytes(message);
await socket.SendAsync(
new ArraySegment<byte>(payload),
WebSocketMessageType.Text,
true,
cancellationToken);
}
}
private static string Dispatch(string command)
{
if (command.TrimStart().StartsWith("{", StringComparison.Ordinal))
{
try
{
return DispatchJson(command);
}
catch (Exception exception)
{
return JsonSerializer.Serialize(new
{
type = "error",
message = exception.Message,
recoverable = true
}, JsonOptions);
}
}
switch (command)
{
case "cmd_state":
return r2warsStatic.torneo.GetStateJson();
case "cmd_prevlog":
return r2warsStatic.r2w.prevLog();
case "cmd_nextlog":
return r2warsStatic.r2w.nextLog();
case "cmd_load":
r2warsStatic.torneo.LoadTournamentPlayers();
break;
case "cmd_reset":
r2warsStatic.torneo.ResetTournament();
break;
case "cmd_run":
r2warsStatic.torneo.RunTournamentCombats();
break;
case "cmd_stop":
r2warsStatic.torneo.StopActualCombat();
break;
case "cmd_step":
case "cmd_next":
r2warsStatic.torneo.StepTournamentCombats();
break;
case "cmd_dbg4":
r2warsStatic.r2w.bStopAtRoundStart = false;
break;
case "cmd_dbg4si":
r2warsStatic.r2w.bStopAtRoundStart = true;
break;
case "cmd_dbg5":
r2warsStatic.r2w.bStopAtRoundEnd = false;
break;
case "cmd_dbg5si":
r2warsStatic.r2w.bStopAtRoundEnd = true;
break;
case "moreflow":
r2warsStatic.r2w.sync_var = true;
return "none";
case "arch_arm":
r2warsStatic.r2w.answer = "arm";
break;
case "arch_x86":
r2warsStatic.r2w.answer = "x86";
break;
}
return null;
}
private static string DispatchJson(string payload)
{
using JsonDocument document = JsonDocument.Parse(payload);
JsonElement root = document.RootElement;
string type = root.GetProperty("type").GetString() ?? "";
switch (type)
{
case "bots":
return JsonSerializer.Serialize(new
{
type = "bots",
warriors = r2warsStatic.torneo.GetWarriorSources()
}, JsonOptions);
case "assemble":
{
int requestId = root.GetProperty("requestId").GetInt32();
BrowserWarriorSource warrior = root.GetProperty("warrior")
.Deserialize<BrowserWarriorSource>(JsonOptions);
WarriorAssemblyResult result = WarriorCompiler.Assemble(warrior);
return JsonSerializer.Serialize(new
{
type = "assembly",
requestId,
ok = result.Ok,
bytes = Array.ConvertAll(result.Bytes, value => (int)value),
size = result.Size,
message = result.Message
}, JsonOptions);
}
case "load":
case "start":
{
BrowserWarriorSource[] warriors = root.GetProperty("warriors")
.Deserialize<BrowserWarriorSource[]>(JsonOptions) ?? Array.Empty<BrowserWarriorSource>();
r2warsStatic.torneo.LoadTournamentSources(warriors);
if (type == "start")
r2warsStatic.torneo.RunTournamentCombats();
return null;
}
default:
throw new InvalidOperationException("Unknown browser message type: " + type);
}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
var e=()=>Array.from({length:1024},()=>``),t=()=>({workflow:`loading`,message:`Loading radare2 WebAssembly…`,player1:{name:`Player 1`,regs:``,code:``},player2:{name:`Player 2`,regs:``,code:``},memory:e(),console:`[r2wars wasm]`,status:`Loading`,scores:`No tournament loaded.`,completedCombats:0,totalCombats:0,activePlayer:-1,historyPosition:0,historyCount:0,canBrowseEarlier:!1,canBrowseLater:!1}),n=[[`.arm-16.`,{id:`arm-16`,arch:`arm`,bits:16}],[`.arm-32.`,{id:`arm-32`,arch:`arm`,bits:32}],[`.arm-64.`,{id:`arm-64`,arch:`arm`,bits:64}],[`.mips-32.`,{id:`mips-32`,arch:`mips`,bits:32}],[`.mips-64.`,{id:`mips-64`,arch:`mips`,bits:64}],[`.x86-32.`,{id:`x86-32`,arch:`x86`,bits:32}],[`.x86-64.`,{id:`x86-64`,arch:`x86`,bits:64}],[`.riscv-32.`,{id:`riscv-32`,arch:`riscv`,bits:32}],[`.riscv-64.`,{id:`riscv-64`,arch:`riscv`,bits:64}],[`.gb.`,{id:`gb`,arch:`gb`,bits:16}],[`.8051.`,{id:`8051`,arch:`8051`,bits:8,cpu:`8051-shared-code-xdata`}]];function r(e){let t=e.toLowerCase(),r=n.find(([e])=>t.includes(e));if(!r)throw Error(`${e}: architecture must be encoded in the filename, for example .x86-32.asm or .arm-32.asm`);return r[1]}function i(e){return e.map(e=>({...e}))}function a(e,t){let n=i(e);for(let e of t){let t=n.findIndex(t=>t.name.toLowerCase()===e.name.toLowerCase());t>=0?n[t]={...e}:n.push({...e})}return n}function o(e){let t=new Set(e.map(e=>e.name.toLowerCase())),n=1;for(;t.has(`new-warrior-${n}.x86-32.asm`);)n++;return{name:`new-warrior-${n}.x86-32.asm`,source:`start:
nop
jmp start
`}}function s(e){if(e.length<2)throw Error(`Add at least two bots before starting a tournament.`);let t=new Set;for(let n of e){let e=n.name.trim();if(!e.toLowerCase().endsWith(`.asm`))throw Error(`${e||`Unnamed bot`}: filename must end in .asm`);r(e);let i=e.toLowerCase();if(t.has(i))throw Error(`Duplicate bot filename: ${e}`);if(t.add(i),!n.source.trim())throw Error(`${e}: source is empty`)}}var c=null,l=0,u=null,d=null,f=!1,p=t(),m=!1,h=!1,g=[],_=-1,v=!1,y=`replace`,b=0,x=e=>{let t=document.getElementById(e);if(!t)throw Error(`Missing UI element #${e}`);return t};function S(e){if(!c||c.readyState!==WebSocket.OPEN)throw Error(`The .NET connection is not ready`);c.send(e.type===`command`?e.command:JSON.stringify(e))}function C(e){m&&(h=!0,S({type:`command`,command:e}),G())}function w(){document.title=`r2wars — .NET arena`;let e=document.createElement(`meta`);e.name=`description`,e.content=`Run multi-architecture r2wars tournaments with the native .NET and radare2 engine.`,document.head.append(e);let t=document.createElement(`style`);t.textContent=`
.connection.local { background:#d8e8ff; color:#174f85; }
.drop-active::after { content:'Drop .asm warriors to load them'; position:fixed; inset:18px; z-index:10; display:grid; place-items:center; border:4px dashed springgreen; background:rgba(0,0,0,.88); color:springgreen; font:700 1.5rem monospace; }
.wasm-note { color:#444; font-size:.72em; margin-right:auto; }
#report_download { margin-left:8px; position:absolute; right:100px; top:24px; width:auto; padding:7px 18px; }
.control-row { grid-template-columns:repeat(3,1fr); }
.bot-manager[hidden] { display:none; }
.bot-manager { position:fixed; inset:2%; z-index:20; display:grid; place-items:center; background:rgba(0,0,0,.84); border:3px solid #999; border-radius:7px; }
.bot-manager-panel { box-sizing:border-box; display:flex; flex-direction:column; width:min(1100px,96%); height:min(760px,94%); overflow:hidden; color:#111; background:#ddd; border:2px solid #eee; box-shadow:0 12px 42px #000; }
.bot-manager-header { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:10px 14px; color:white; background:linear-gradient(90deg,#123b8d,#8b1717); }
.bot-manager-header h2 { margin:0; font-size:1.15rem; }
.bot-manager-header span { font-size:.78rem; }
.bot-manager-body { min-height:0; flex:1; display:grid; grid-template-columns:minmax(220px,30%) 1fr; gap:10px; padding:10px; }
.bot-sidebar,.bot-editor { min-height:0; display:flex; flex-direction:column; gap:8px; }
.bot-editor.no-selection > :not(.bot-editor-empty) { display:none; }
.bot-list { min-height:120px; flex:1; overflow:auto; padding:4px; background:#171717; border:1px inset #aaa; }
.bot-list button { margin-bottom:4px; padding:7px 8px; overflow:hidden; text-align:left; text-overflow:ellipsis; white-space:nowrap; color:#e8e8e8; background:#292929; border:1px solid #555; }
.bot-list button.selected { color:#111; background:#9ed1ff; border-color:#3275aa; font-weight:bold; }
.bot-sidebar-actions,.bot-manager-footer { display:flex; gap:8px; }
.bot-sidebar-actions button,.bot-manager-footer button { width:auto; padding:7px 14px; }
.bot-editor label { font-size:.8rem; font-weight:bold; }
.bot-editor input,.bot-editor textarea { box-sizing:border-box; width:100%; color:#d8ffd8; background:#111; border:1px inset #aaa; font:14px/1.4 Consolas,'Courier New',monospace; }
.bot-editor input { padding:7px; }
.bot-editor textarea { min-height:180px; flex:1; padding:10px; resize:none; tab-size:2; }
.bot-assembly { min-height:84px; max-height:150px; display:flex; flex-direction:column; gap:5px; overflow:auto; padding:7px; color:#ddd; background:#171717; border:1px inset #aaa; }
.bot-assembly-summary { display:flex; align-items:center; gap:7px; font-size:.78rem; }
.bot-assembly-summary strong { min-width:0; overflow-wrap:anywhere; }
.bot-compile-indicator { width:11px; height:11px; flex:0 0 11px; border-radius:50%; background:#777; box-shadow:0 0 5px #555; }
.bot-compile-indicator.pending { background:#e5b72f; box-shadow:0 0 6px #e5b72f; }
.bot-compile-indicator.valid { background:#39e75f; box-shadow:0 0 6px #39e75f; }
.bot-compile-indicator.invalid { background:#f04444; box-shadow:0 0 6px #f04444; }
.bot-assembly-size { margin-left:auto; color:#9ed1ff; white-space:nowrap; }
.bot-byte-array { min-height:38px; margin:0; overflow:auto; color:#b7e6ff; font:12px/1.35 Consolas,'Courier New',monospace; white-space:pre-wrap; word-break:break-all; }
.bot-editor-empty { display:grid; flex:1; place-items:center; color:#555; border:1px dashed #999; }
.bot-manager-footer { align-items:center; padding:9px 12px; border-top:1px solid #999; }
.bot-manager-status { min-height:1.2em; flex:1; color:#444; font-size:.78rem; }
.bot-manager-status.error { color:#8c1010; font-weight:bold; }
#bot_start { background:#d8f5de; border-color:#318244; font-weight:bold; }
@media (max-width: 900px), (orientation: portrait) {
.bot-manager { inset:0; border:0; }
.bot-manager-panel { height:100%; width:100%; }
.bot-manager-body { grid-template-columns:1fr; grid-template-rows:minmax(150px,32%) 1fr; }
.bot-manager-header span { display:none; }
}
`,document.head.append(t);let n=document.createElement(`input`);n.type=`file`,n.accept=`.asm,text/plain`,n.multiple=!0,n.hidden=!0,n.setAttribute(`aria-label`,`Choose warrior assembly files`),document.body.append(n);let r=document.querySelector(`.inspection-row`);if(r){let e=document.createElement(`span`);e.className=`wasm-note`,e.textContent=`Native engine · drop .asm files anywhere`,r.prepend(e)}let i=document.createElement(`button`);i.id=`cmd_bots`,i.textContent=`Bots`,i.title=`Add, remove, or edit bots for the next tournament`,document.querySelector(`.control-row`)?.append(i);let a=document.createElement(`div`);a.id=`bot_manager`,a.className=`bot-manager`,a.hidden=!0,a.setAttribute(`role`,`dialog`),a.setAttribute(`aria-modal`,`true`),a.setAttribute(`aria-labelledby`,`bot_manager_title`),a.innerHTML=`
<div class="bot-manager-panel">
<header class="bot-manager-header">
<h2 id="bot_manager_title">Bot manager</h2>
<span>Draft changes are loaded only when a new tournament starts</span>
</header>
<div class="bot-manager-body">
<aside class="bot-sidebar">
<strong id="bot_count">0 bots</strong>
<div id="bot_list" class="bot-list" role="listbox" aria-label="Loaded bots"></div>
<div class="bot-sidebar-actions">
<button id="bot_add" type="button">New bot</button>
<button id="bot_import" type="button">Import .asm</button>
</div>
</aside>
<section class="bot-editor">
<label for="bot_name">Filename and architecture</label>
<input id="bot_name" type="text" autocomplete="off" spellcheck="false" placeholder="name.x86-32.asm">
<label for="bot_source">Assembly source</label>
<textarea id="bot_source" spellcheck="false" aria-label="Bot assembly source"></textarea>
<div class="bot-assembly" aria-live="polite">
<div class="bot-assembly-summary">
<span id="bot_compile_indicator" class="bot-compile-indicator" aria-hidden="true"></span>
<strong id="bot_compile_status">Not checked</strong>
<span id="bot_assembly_size" class="bot-assembly-size">0 bytes</span>
</div>
<pre id="bot_byte_array" class="bot-byte-array">[]</pre>
</div>
<div class="bot-sidebar-actions">
<button id="bot_delete" type="button">Delete selected bot</button>
</div>
<div id="bot_editor_empty" class="bot-editor-empty" hidden>Select a bot to edit its source.</div>
</section>
</div>
<footer class="bot-manager-footer">
<span id="bot_manager_status" class="bot-manager-status" role="status"></span>
<button id="bot_close" type="button">Close</button>
<button id="bot_start" type="button">Start new tournament</button>
</footer>
</div>`,document.body.append(a);let o=document.createElement(`button`);return o.id=`report_download`,o.textContent=`Download report`,o.hidden=!0,x(`overlay`).append(o),o.addEventListener(`click`,re),n}var T=w();T.addEventListener(`change`,async()=>{if(T.files?.length){let e=await D(Array.from(T.files));g=y===`replace`?e:a(g,e);let t=e.at(-1)?.name.toLowerCase();_=t?g.findIndex(e=>e.name.toLowerCase()===t):-1,v=!0,j()}T.value=``}),x(`cmd_load`).addEventListener(`click`,()=>{y=`replace`,T.click()}),x(`cmd_bots`).addEventListener(`click`,j),x(`cmd_run`).addEventListener(`click`,()=>{p.workflow===`running`?C(`cmd_stop`):p.workflow===`paused`?C(`cmd_run`):A()}),x(`cmd_step`).addEventListener(`click`,()=>C(`cmd_step`)),x(`cmd_reset`).addEventListener(`click`,()=>{(p.workflow!==`running`&&p.workflow!==`paused`||window.confirm(`Reset this tournament? Current progress and scores will be cleared.`))&&ne()}),x(`cmd_scores`).addEventListener(`click`,X),x(`log_prev`).addEventListener(`click`,()=>C(`cmd_prevlog`)),x(`log_next`).addEventListener(`click`,()=>C(`cmd_nextlog`)),x(`score_close`).addEventListener(`click`,Q),x(`nowarriorsclose`).addEventListener(`click`,$),x(`bot_add`).addEventListener(`click`,N),x(`bot_import`).addEventListener(`click`,()=>{y=`merge`,T.click()}),x(`bot_delete`).addEventListener(`click`,P),x(`bot_close`).addEventListener(`click`,M),x(`bot_start`).addEventListener(`click`,A),x(`bot_name`).addEventListener(`input`,e=>{_<0||(g[_].name=e.currentTarget.value,v=!0,I(),R(),G(),z())}),x(`bot_source`).addEventListener(`input`,e=>{_<0||(g[_].source=e.currentTarget.value,v=!0,R(),z())}),x(`overlay`).addEventListener(`click`,e=>{e.target===x(`overlay`)&&Q()}),x(`bot_manager`).addEventListener(`click`,e=>{e.target===x(`bot_manager`)&&M()}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&(x(`bot_manager`).hidden?(Q(),$()):M())}),document.addEventListener(`dragover`,e=>{e.preventDefault(),p.workflow!==`running`&&document.body.classList.add(`drop-active`)}),document.addEventListener(`dragleave`,e=>{e.relatedTarget||document.body.classList.remove(`drop-active`)}),document.addEventListener(`drop`,async e=>{if(e.preventDefault(),document.body.classList.remove(`drop-active`),p.workflow===`running`)return;let t=Array.from(e.dataTransfer?.files??[]).filter(e=>e.name.toLowerCase().endsWith(`.asm`));if(t.length){let e=await D(t);g=a(g,e);let n=e.at(-1)?.name.toLowerCase();_=n?g.findIndex(e=>e.name.toLowerCase()===n):-1,v=!0,j()}});async function E(e){e.type===`ready`?(m=!0,x(`connection`).textContent=`.NET server`,x(`connection`).className=`connection local`,f?S({type:`command`,command:`cmd_state`}):(f=!0,await O())):e.type===`bots`?(u?.(e.warriors),u=null,d=null):e.type===`assembly`?B(e):e.type===`state`?(h=!1,p=ee(p,e.state),K()):e.recoverable?(h=!1,d?.(Error(e.message)),u=null,d=null,f&&(j(),R(e.message)),G()):V(e.message)}function ee(e,t){return{...e,...t,player1:{...e.player1,...t.player1},player2:{...e.player2,...t.player2},memory:t.memory??e.memory}}async function D(e){return Promise.all(e.map(async e=>({name:e.name,source:await e.text()})))}async function O(){try{g=i(await te()),_=g.length?0:-1,v=!1,S({type:`load`,warriors:i(g)})}catch(e){p.workflow=`idle`,p.message=`Choose at least two warrior files. Bundled examples could not be loaded: ${e instanceof Error?e.message:e}`,K()}}function te(){return new Promise((e,t)=>{u=e,d=t,c?.send(JSON.stringify({type:`bots`})),window.setTimeout(()=>{u===e&&(u=null,d=null,t(Error(`Timed out while loading bots from the .NET server`)))},5e3)})}function k(){return g.map(e=>({name:e.name.trim(),source:e.source}))}function A(){try{let e=k();s(e),g=i(e),v=!1,h=!0,M(),S({type:`start`,warriors:i(g)}),G()}catch(e){j(),R(e instanceof Error?e.message:String(e))}}function ne(){try{let e=k();s(e),g=i(e),v=!1,h=!0,S({type:`load`,warriors:i(g)}),G()}catch(e){j(),R(e instanceof Error?e.message:String(e))}}function j(){if(!m||p.workflow===`running`)return;let e=x(`bot_manager`);e.hidden=!1,L(),_>=0?x(`bot_name`).focus():x(`bot_add`).focus()}function M(){x(`bot_manager`).hidden=!0,x(`cmd_bots`).focus({preventScroll:!0})}function N(){g.push(o(g)),_=g.length-1,v=!0,L();let e=x(`bot_name`);e.focus(),e.select()}function P(){if(_<0)return;let e=g[_].name||`this bot`;window.confirm(`Delete ${e} from the next tournament?`)&&(g.splice(_,1),_=g.length?Math.min(_,g.length-1):-1,v=!0,L())}function F(e){_=e,L(),x(`bot_source`).focus()}function I(){let e=x(`bot_list`);e.replaceChildren(),g.forEach((t,n)=>{let r=document.createElement(`button`);r.type=`button`,r.textContent=t.name||`Unnamed bot`,r.title=t.name||`Unnamed bot`,r.classList.toggle(`selected`,n===_),r.setAttribute(`role`,`option`),r.setAttribute(`aria-selected`,String(n===_)),r.addEventListener(`click`,()=>F(n)),e.append(r)}),x(`bot_count`).textContent=`${g.length} bot${g.length===1?``:`s`}`,x(`cmd_bots`).textContent=`Bots (${g.length})`}function L(){_>=g.length&&(_=g.length-1),I();let e=_>=0?g[_]:void 0;document.querySelector(`Line truncated
+722
View File
@@ -0,0 +1,722 @@
<!doctype html>
<html lang="en" data-r2wars-engine="dotnet">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Run multi-architecture r2wars tournaments with radare2.">
<title>r2wars</title>
<style>
button {
min-width: 10px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.connection {
border-radius: 10px;
color: #333;
font-size: 0.78em;
padding: 2px 7px;
}
.connection.connected {
background: #c7f4d0;
color: #145b23;
}
.connection.connecting {
background: #fff2b7;
}
.connection.disconnected {
background: #ffd0d0;
color: #7b1111;
}
table {
}
button {
width: 100%;
}
.addr {
color: white;
text-align: center;
background-color: black;
font-weight: bold;
width: 6em;
}
.div1 {
position: absolute;
top: 0px;
left: 0px;
width: 30%;
height: 100%;
overflow-x: hidden;
}
.divM3 {
position: fixed;
bottom: 0px;
background-color: #e0e0e0;
left: 30%;
right: 30%;
width: 40%;
height: 16em;
overflow: hidden;
}
.divM2 {
position: fixed;
bottom: 16em;
background-color: #e0e0e0;
left: 30%;
right: 30%;
width: 40%;
height: 132px;
box-sizing: border-box;
border: 1px solid #a0a0a0;
padding: 7px;
overflow: hidden;
}
.status-strip, .inspection-row {
align-items: center;
display: flex;
gap: 7px;
}
.status-strip {
justify-content: space-between;
}
.workflow-badge {
background: #d2d2d2;
border-radius: 10px;
font-size: 0.78em;
font-weight: bold;
padding: 2px 8px;
text-transform: uppercase;
white-space: nowrap;
}
.workflow-badge.ready { background: #d5e9ff; color: #174f85; }
.workflow-badge.running { background: #c7f4d0; color: #145b23; }
.workflow-badge.paused { background: #fff2b7; color: #705600; }
.workflow-badge.finished { background: #d9ccff; color: #41257c; }
.progress {
flex: 1;
font-size: 0.78em;
font-weight: bold;
}
.workflow-message {
height: 30px;
line-height: 15px;
margin: 5px 1px;
overflow: hidden;
}
.stage-notice {
background: rgba(18, 18, 18, 0.92);
border: 2px solid #aaa;
border-radius: 6px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55);
color: white;
left: 8%;
padding: 18px 22px;
position: absolute;
right: 8%;
text-align: center;
top: 18%;
z-index: 1;
}
.stage-notice[hidden] { display: none; }
.stage-notice strong {
color: springgreen;
display: block;
font-size: 1.55em;
margin-bottom: 8px;
}
.stage-notice.ready strong { color: #8fc7ff; }
.stage-notice.finished strong { color: #c9b4ff; }
.control-row {
display: grid;
gap: 5px;
grid-template-columns: 1.3fr 1.4fr 0.8fr 0.8fr 0.8fr;
}
.control-row button, .inspection-row button {
background: #f3f3f3;
border: 1px solid #777;
border-radius: 3px;
min-height: 25px;
}
.control-row button.primary {
background: #d8f5de;
border-color: #318244;
font-weight: bold;
}
.control-row button.primary.pause {
background: #fff0bd;
border-color: #9a761c;
}
.inspection-row {
font-size: 0.75em;
justify-content: flex-end;
margin-top: 5px;
}
.inspection-row button {
max-width: 92px;
}
.divM {
position: absolute;
top: 0px;
left: 30%;
right: 30%;
height: 100%;
width: 40%;
overflow-x: scroll;
}
.div2 {
position: absolute;
top: 0px;
right: 0px;
width: 30%;
height: 100%;
overflow-x: hidden;
}
.memory {
overflow-y: hidden;
height: 100%;
background-color: #303030;
}
.box {
width: 1em;
height: 1em;
text-align: center;
color: white;
background-color: #a0a0a0;
font-weight: bold;
}
.console {
height: 14.5em;
background-color: black;
color: white;
font-family: Courier New;
font-size: 14px;
font-weight: bold;
white-space: pre;
overflow-y: scroll;
overflow-x: scroll;
}
.console1 {
height: 238px;
background-color: black;
width: 100%;
color: white;
font-family: Courier New;
font-size: 15px;
font-weight: bold;
white-space: pre;
overflow-y: hidden;
overflow-x: hidden;
}
.console2 {
height: 100%;
background-color: black;
color: springgreen;
font-family: Courier New;
font-size: 13px;
font-weight: bold;
width: 100%;
white-space: pre;
overflow-x: hidden;
overflow-y: hidden;
}
.s {
background-color: green;
color: black;
font-family: Courier New;
font-weight: bold;
white-space: pre;
}
body {
overflow: hidden;
background-color: #909090;
}
td {
font-family: Courier New;
border: 1px solid #a0a0a0;
color: black;
}
button {
min-width: 1px;
background-color: inherit;
}
div {
font-family: Courier New;
}
tr {
background-color: #e0e0e0;
}
.player1 {
background-color: blue;
width: 100%;
color: white;
font-weight: bold;
font-size: 2em;
font-family: Consolas, Courier New;
overflow-x: hidden;
text-align: left;
padding-left: 2em;
}
.player2 {
color: white;
width: 100%;
background-color: red;
font-weight: bold;
font-size: 2em;
font-family: Consolas, Courier New;
overflow-x: hidden;
text-align: left;
padding-left: 0em;
}
.active-dot {
display: inline-block;
width: 0.8em;
height: 0.8em;
margin: 0 0.5em;
border: 2px solid white;
border-radius: 50%;
background-color: #4b4b4b;
vertical-align: middle;
}
.active-dot.active {
background-color: #39e75f;
box-shadow: 0 0 0.5em #39e75f;
animation: active-pulse 0.8s ease-in-out infinite alternate;
}
@keyframes active-pulse {
from { transform: scale(0.85); }
to { transform: scale(1.15); }
}
.memtable {
font-size: 10px;
width: 100%;
}
.overlay {
display: none;
top: 1%;
width: 90%;
right: 5%;
bottom: 1%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 2;
position: absolute;
border-radius: 6px;
border: 3px solid #a0a0a0;
}
.text {
font-weight: bold;
margin: 24px 80px 12px 30px;
font-size: 40px;
color: springgreen;
}
.text1 {
font-weight: bold;
font-size: 1.4em;
color: white;
white-space: pre;
margin: 20px 30px;
max-height: calc(100% - 130px);
overflow: auto;
}
.overlay-close {
background: #e5e5e5;
border: 1px solid #888;
border-radius: 3px;
padding: 7px 18px;
position: absolute;
right: 24px;
top: 24px;
width: auto;
}
.win98 {
display: none;
top: 30%;
left: 30%;
right: 30%;
z-index: 3;
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: none;
font-size: 11px;
font-family: Tahoma, Verdana;
color: #070707;
position: absolute;
margin: 0 auto;
background: #bfbfbf;
border: 1px solid;
border-top-color: #dfdfdf;
border-left-color: #dfdfdf;
border-right-color: #808080;
border-bottom-color: #808080;
box-shadow: 1px 1px 0 0 #000;
width: 400px;
cursor: default;
}
.win98 .content {
padding: 7px 10px;
font-size: 16px;
}
.win98 .header {
margin: 2px 2px 0;
padding: 1px 2px;
color: #fff;
background: -webkit-linear-gradient(left, blue, red);
background: linear-gradient(to right, blue, red);
font-weight: bold;
font-size: 16px;
line-height: 16px;
}
.legend1 {
position: absolute;
top: 10px;
z-index: 2;
background-color: black;
left: 10px;
font-weight: bold;
text-align: center;
}
.legend2 {
position: absolute;
top: 10px;
z-index: 2;
background-color: black;
right: 10px;
font-weight: bold;
text-align: left;
}
.legend {
font-weight: bold;
text-align: center;
width: 10px;
max-width: 10px;
height: 1em;
}
.win98 button {
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: none;
font-size: 11px;
font-family: Tahoma, Verdana;
color: #070707;
background: #bfbfbf;
border: 1px solid;
border-top-color: #dfdfdf;
border-left-color: #dfdfdf;
border-right-color: #808080;
border-bottom-color: #808080;
box-shadow: 1px 1px 0 0 #000;
text-align: center;
outline: none;
padding: 4px;
cursor: default;
font-size: 16px;
}
.win98 button:enabled {
color: #070707;
}
.win98 button:disabled {
color: #818181;
text-shadow: 1px 1px #fff;
}
.win98 button:active:enabled {
box-shadow: 1px 1px 0 0 #000 inset;
border-top-color: #808080;
border-left-color: #808080;
border-right-color: #dfdfdf;
border-bottom-color: #dfdfdf;
}
/* On narrow or portrait screens, keep the arena visible before the code. */
@media (max-width: 900px), (orientation: portrait) {
body {
margin: 0;
overflow: auto;
}
.div1, .div2, .divM, .divM2, .divM3 {
box-sizing: border-box;
left: auto;
position: relative;
right: auto;
top: auto;
width: 100%;
}
.div1, .div2 {
height: 180px;
min-height: 0;
overflow: hidden !important;
}
.div1 table, .div2 table {
height: 100% !important;
table-layout: fixed;
}
.player1, .player2 {
box-sizing: border-box;
font-size: 1.1em;
line-height: 1.5;
overflow: hidden;
padding-left: 0.5em;
text-overflow: ellipsis;
white-space: nowrap;
}
.active-dot {
height: 0.6em;
margin: 0 0.35em;
width: 0.6em;
}
.console1 {
box-sizing: border-box;
font-size: 12px;
height: 44px;
overflow: auto;
}
.console2 {
box-sizing: border-box;
font-size: 11px;
height: 94px;
line-height: 1.2;
min-height: 0;
overflow: auto;
}
.divM {
height: min(440px, 115vw);
min-height: 0;
overflow: auto;
}
.divM table {
height: 100% !important;
table-layout: fixed;
}
.memory {
min-width: 0;
}
.memtable {
font-size: 9px;
table-layout: fixed;
}
.addr {
width: 4em;
}
.box {
height: 0.9em;
padding: 0;
width: auto;
}
.divM2 {
bottom: auto;
min-height: 165px;
}
.divM3 {
bottom: auto;
height: 180px;
}
.control-row {
grid-template-columns: repeat(3, 1fr);
}
.legend1, .legend2 {
display: none;
}
}
</style>
</head>
<body>
<div id="overlay" class="overlay" role="dialog" aria-modal="true" aria-labelledby="score_title">
<div id="score_title" class="text">Tournament scores</div>
<button id="score_close" class="overlay-close">Close</button>
<pre id="scores" class="text1"></pre>
</div>
<div id="nowarriorsdlg" class="win98" role="dialog" aria-modal="true" aria-labelledby="warriors-title">
<div class="header">
<span id="warriors-title">Warriors not found</span>
</div>
<div class="content">
<center>
<h3>Add at least two .asm warrior files to the warriors directory, then load again.</h3>
<button id="nowarriorsclose">Close</button>
</center>
</div>
</div>
<div class="legend1">
<table>
<tbody>
<tr>
<td class="legend" style="background-color:aqua">R</td>
<td class="legend" style="background-color:violet">W</td>
</tr>
</tbody>
</table>
</div>
<div class="div1" style="overflow-y:hidden;">
<table style="height:100%;width:100%">
<tbody>
<tr>
<td class="player1"><span id="active1" class="active-dot" aria-label="Player 1 is waiting"></span><span id="player1">Player1</span></td>
</tr>
<tr style="height:100%">
<td valign="top" style="overflow-x:hidden;overflow-y:hidden">
<div class="console1" id="regs1"></div>
<div style="border: 1px solid #a0a0a0"></div>
<div class="console2" id="code1"></div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="divM" style="overflow-x:hidden">
<div id="stage_notice" class="stage-notice idle">
<strong id="stage_title">No tournament loaded</strong>
<span id="stage_detail">Load warriors to begin.</span>
</div>
<center class="">
<table style="height:100%;overflow-x:scroll;width:100%">
<tbody>
<tr style="height:100%">
<td valign="top"><div class="memory" id="memory"></div></td>
</tr>
</tbody>
</table>
</center>
</div>
<div class="divM2">
<div class="status-strip">
<span id="workflow_badge" class="workflow-badge idle">idle</span>
<span id="progress" class="progress">0 / 0 battles</span>
<span id="connection" class="connection connecting" role="status">Connecting…</span>
</div>
<div id="workflow_message" class="workflow-message">Load warriors to begin.</div>
<div class="control-row">
<button id="cmd_load" title="Scan the warriors directory for .asm files">Load warriors</button>
<button id="cmd_run" class="primary" title="Start or pause continuous tournament play">Start tournament</button>
<button id="cmd_step" title="Advance exactly one engine cycle while paused">Step</button>
<button id="cmd_scores" title="Show current standings">Scores</button>
<button id="cmd_reset" title="Clear progress and scores for the loaded warriors">Reset</button>
</div>
<div class="inspection-row">
<span>Execution history (paused):</span>
<button id="log_prev" title="Show the earlier recorded state for both warriors">Earlier state</button>
<button id="log_next" title="Show the later recorded state for both warriors">Later state</button>
</div>
</div>
<div class="divM3">
<center class="">
<table style="width:100%">
<tbody>
<tr style="height:100%">
<td style="height:100%">
<div id="console" class="console"></div>
</td>
</tr>
<tr style="height:100%">
<td id="status">Idle</td>
</tr>
</tbody>
</table>
</center>
</div>
<div class="legend2">
<table>
<tbody>
<tr>
<td class="legend" style="background-color:yellow">R</td>
<td class="legend" style="background-color:orange">W</td>
</tr>
</tbody>
</table>
</div>
<div class="div2" style="overflow-y:hidden">
<table style="height:100%;width:100%">
<tbody>
<tr>
<td class="player2"><span id="active2" class="active-dot" aria-label="Player 2 is waiting"></span><span id="player2">Player2</span></td>
</tr>
<tr style="height:100%">
<td valign="top" style="overflow-x:hidden;overflow-y:hidden" height="100%" colspan="3">
<div class="console1" id="regs2"></div>
<div style="border: 1px solid #a0a0a0"></div>
<div class="console2" id="code2"></div>
</td>
</tr>
</tbody>
</table>
</div>
<script type="module" src="/assets/r2wars-ui.js"></script>
</body>
</html>
+8 -13
View File
@@ -3,22 +3,19 @@
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ASSEMBLY_INFO="$SCRIPT_DIR/csharp/Properties/AssemblyInfo.cs"
PROJECT_FILE="$SCRIPT_DIR/csharp/r2wars.csproj"
if [ ! -f "$ASSEMBLY_INFO" ]; then
printf '%s\n' "version.sh: missing assembly file: $ASSEMBLY_INFO" >&2
if [ ! -f "$PROJECT_FILE" ]; then
printf '%s\n' "version.sh: missing project file: $PROJECT_FILE" >&2
exit 1
fi
assembly_version=$(sed -n 's/^[[:space:]]*\[assembly:[[:space:]]*AssemblyVersion("\([0-9][0-9.]*\)")\][[:space:]]*$/\1/p' "$ASSEMBLY_INFO")
if [ -z "$assembly_version" ]; then
current_version=$(sed -n 's|^[[:space:]]*<Version>\([0-9][0-9.]*\)</Version>[[:space:]]*$|\1|p' "$PROJECT_FILE")
if [ -z "$current_version" ]; then
printf '%s\n' "version.sh: cannot read the current version" >&2
exit 1
fi
# .NET assembly versions have a fourth revision component. Release versions do not.
current_version=${assembly_version%.0}
if [ "$#" -eq 0 ]; then
printf '%s\n' "$current_version"
exit 0
@@ -35,14 +32,12 @@ if ! printf '%s\n' "$new_version" | grep -Eq '^[0-9]+(\.[0-9]+){2}$'; then
exit 1
fi
new_assembly_version="$new_version.0"
temporary_file=$(mktemp "${TMPDIR:-/tmp}/r2wars-version.XXXXXX")
trap 'rm -f "$temporary_file"' EXIT HUP INT TERM
sed -E \
-e "s/^([[:space:]]*\[assembly:[[:space:]]*AssemblyVersion\(\")[0-9.]+(\"\)\][[:space:]]*)$/\1${new_assembly_version}\2/" \
-e "s/^([[:space:]]*\[assembly:[[:space:]]*AssemblyFileVersion\(\")[0-9.]+(\"\)\][[:space:]]*)$/\1${new_assembly_version}\2/" \
"$ASSEMBLY_INFO" > "$temporary_file"
cp "$temporary_file" "$ASSEMBLY_INFO"
-e "s|^([[:space:]]*<Version>)[0-9.]+(</Version>[[:space:]]*)$|\1${new_version}\2|" \
"$PROJECT_FILE" > "$temporary_file"
cp "$temporary_file" "$PROJECT_FILE"
printf '%s -> %s\n' "$current_version" "$new_version"
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
.site/
index.html
public/radare2.wasm
r2wars-wasm.zip
+62
View File
@@ -0,0 +1,62 @@
# r2wars WebAssembly
This directory contains the shared TypeScript web client and the standalone
browser engine. The same client is compiled for both the C# WebSocket transport
and the local Web Worker transport.
## Architecture
- `src/main.ts` owns the shared interface and selects the .NET WebSocket or Wasm
Worker transport at build time.
- `src/bots.ts` manages and validates the editable draft bot roster.
- `src/worker.ts` owns the tournament controller and keeps ESIL work off the UI
thread.
- `src/tournament.ts` implements round-robin scheduling, scoring, pause/step,
bounded history, timeouts, and downloadable reports.
- `src/engine.ts` implements the two-warrior shared arena and synchronizes ESIL
memory writes between two `RCore` objects.
- `src/radare.ts` is the small WASI/FFI wrapper around radare2.
The build reuses the maintained UI markup and styles from
`csharp/wwwroot/index.html`. It produces the standalone site in `wasm/dist/`
and the native client bundle in `csharp/wwwroot/assets/r2wars-ui.js` from the
same TypeScript source.
## Commands
npm install
npm test
npm run build
npm run dev
npm run build:dotnet
`npm run build` produces a self-contained static directory at `dist/`.
From the repository root, `make wasm-dist` also creates
`wasm/r2wars-wasm.zip`. Extract its contents directly into a static server's
document root; `index.html` is at the archive root.
The **Bots** dialog can create, import, rename, edit, and delete browser-local
warriors. These edits remain in a draft roster and are sent to the worker only
when a new tournament starts, so they cannot mutate an active combat. Each
editor input event also makes an isolated assembly request and displays the
radare2 result, byte count, and byte array without loading that code into the
tournament engine.
## radare2 artifact
`scripts/fetch-r2.mjs` downloads `radare2-6.2.0-wasi-api.zip` from the official
radare2 release, verifies both the archive and module SHA-256 digests, and puts
the extracted module in `public/radare2.wasm`. The large generated module and
build output are intentionally ignored by git.
The released WASI plugin set covers x86, ARM, MIPS, RISC-V, and Game Boy
warriors. The 6.2.0 WASI release does not include the 8051 plugin, so 8051
warriors currently report an assembly error in the browser build; the native
.NET/Docker path retains 8051 support.
## Static and offline use
The `dist/` directory requires no application backend. Serve it from any
ordinary static host. Browsers generally restrict `file://` WebAssembly module
loading, so opening `index.html` directly from disk is not a supported launch
method.
+1632
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "r2wars-wasm",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"fetch-r2": "node scripts/fetch-r2.mjs",
"prepare-index": "node scripts/prepare-index.mjs",
"dev": "npm run fetch-r2 && npm run prepare-index && vite",
"build": "npm run fetch-r2 && npm run prepare-index && tsc --noEmit && vite build && npm run build:dotnet",
"build:dotnet": "vite build --config vite.dotnet.config.ts",
"test": "npm run fetch-r2 && vitest run --config vitest.config.ts"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "0.4.2"
},
"devDependencies": {
"@types/node": "^26.4.0",
"fflate": "0.8.3",
"typescript": "7.0.2",
"vite": "8.2.2",
"vitest": "4.1.11"
}
}
@@ -0,0 +1,8 @@
call getpc
getpc:
pop edi
add edi, 128
loop:
mov dword ptr [edi], 0xcccccccc
add edi, 16
jmp loop
@@ -0,0 +1,7 @@
call getpc
getpc:
pop eax
loop:
sub eax, 8
mov dword ptr [eax], 0
jmp loop
+9
View File
@@ -0,0 +1,9 @@
call label
label:
pop eax
loop:
sub eax, 10
cmp dword ptr[eax], 0
je loop
mov dword ptr[eax], 0
jmp loop
+5
View File
@@ -0,0 +1,5 @@
[
"jordi.x86-32.asm",
"browser-sweeper.x86-32.asm",
"browser-striker.x86-32.asm"
]
+34
View File
@@ -0,0 +1,34 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { unzipSync } from "fflate";
const version = "6.2.0";
const archiveSha256 = "f7ce3b9919cc526ab6d6dbb2c5a12783781a0097b74d8de608b5639201c75db6";
const wasmSha256 = "d583b19828317aabdd7de942d470b6fe1b7370929763ff38483158157c7ef3ae";
const output = join(import.meta.dirname, "..", "public", "radare2.wasm");
const source = `https://github.com/radareorg/radare2/releases/download/${version}/radare2-${version}-wasi-api.zip`;
const digest = (data) => createHash("sha256").update(data).digest("hex");
try {
const present = await readFile(output);
if (digest(present) === wasmSha256) process.exit(0);
} catch {
// Fetch below.
}
console.log(`Downloading radare2 ${version} WASI API…`);
const response = await fetch(source);
if (!response.ok) throw new Error(`radare2 download failed: ${response.status} ${response.statusText}`);
const archive = new Uint8Array(await response.arrayBuffer());
if (digest(archive) !== archiveSha256) throw new Error("radare2 archive checksum mismatch");
const files = unzipSync(archive);
const entry = Object.entries(files).find(([name]) => name.endsWith("/radare2.wasm"));
if (!entry) throw new Error("radare2.wasm was not found in the release archive");
if (digest(entry[1]) !== wasmSha256) throw new Error("radare2.wasm checksum mismatch");
await mkdir(join(import.meta.dirname, "..", "public"), { recursive: true });
await writeFile(output, entry[1]);
console.log(`Saved ${output}`);
+13
View File
@@ -0,0 +1,13 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const root = join(import.meta.dirname, "..");
const sourcePath = join(root, "..", "csharp", "wwwroot", "index.html");
const outputPath = join(root, "index.html");
let html = await readFile(sourcePath, "utf8");
html = html.replace('data-r2wars-engine="dotnet"', 'data-r2wars-engine="wasm"');
html = html.replace('/assets/r2wars-ui.js', './src/main.ts');
await mkdir(root, { recursive: true });
await writeFile(outputPath, html);
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { architectureFromFilename, r2ArchitectureCommands } from "./architecture";
import { parseCycles, parseMemoryAccess } from "./engine";
describe("architecture detection", () => {
it("maps every supported filename family", () => {
const mappings = [
["foo.x86-32.asm", "x86", 32],
["foo.x86-64.asm", "x86", 64],
["foo.arm-16.asm", "arm", 16],
["foo.arm-32.asm", "arm", 32],
["foo.arm-64.asm", "arm", 64],
["foo.mips-32.asm", "mips", 32],
["foo.mips-64.asm", "mips", 64],
["foo.riscv-32.asm", "riscv", 32],
["foo.riscv-64.asm", "riscv", 64],
["foo.gb.asm", "gb", 16],
["foo.8051.asm", "8051", 8],
] as const;
for (const [filename, arch, bits] of mappings) {
expect(architectureFromFilename(filename)).toMatchObject({ arch, bits });
}
expect(r2ArchitectureCommands(architectureFromFilename("foo.8051.asm"))).toContain("asm.cpu=8051-shared-code-xdata");
});
it("rejects untagged sources", () => {
expect(() => architectureFromFilename("foo.asm")).toThrow(/architecture/);
});
});
describe("radare output parsing", () => {
it("parses ESIL memory flags", () => {
expect(parseMemoryAccess("f mem.read.0 0x00000004 @ 0x00000020\nf mem.write.1 0x2 @ 0x3f")).toEqual([
{ kind: "read", size: 4, address: 0x20 },
{ kind: "write", size: 2, address: 0x3f },
]);
});
it("parses instruction cycles safely", () => {
expect(parseCycles('[{"cycles":3,"size":1}]')).toBe(3);
expect(parseCycles("not json")).toBe(0);
});
});
+39
View File
@@ -0,0 +1,39 @@
export interface Architecture {
id: string;
arch: string;
bits: number;
cpu?: string;
}
const architectures: Array<[string, Architecture]> = [
[".arm-16.", { id: "arm-16", arch: "arm", bits: 16 }],
[".arm-32.", { id: "arm-32", arch: "arm", bits: 32 }],
[".arm-64.", { id: "arm-64", arch: "arm", bits: 64 }],
[".mips-32.", { id: "mips-32", arch: "mips", bits: 32 }],
[".mips-64.", { id: "mips-64", arch: "mips", bits: 64 }],
[".x86-32.", { id: "x86-32", arch: "x86", bits: 32 }],
[".x86-64.", { id: "x86-64", arch: "x86", bits: 64 }],
[".riscv-32.", { id: "riscv-32", arch: "riscv", bits: 32 }],
[".riscv-64.", { id: "riscv-64", arch: "riscv", bits: 64 }],
[".gb.", { id: "gb", arch: "gb", bits: 16 }],
[".8051.", { id: "8051", arch: "8051", bits: 8, cpu: "8051-shared-code-xdata" }],
];
export function architectureFromFilename(filename: string): Architecture {
const lower = filename.toLowerCase();
const match = architectures.find(([marker]) => lower.includes(marker));
if (!match) {
throw new Error(`${filename}: architecture must be encoded in the filename, for example .x86-32.asm or .arm-32.asm`);
}
return match[1];
}
export function r2ArchitectureCommands(architecture: Architecture): string {
const commands = [`e asm.arch=${architecture.arch}`, `e asm.bits=${architecture.bits}`];
if (architecture.cpu) commands.push(`e asm.cpu=${architecture.cpu}`);
return commands.join(";");
}
export function displayName(filename: string): string {
return filename.replace(/\.asm$/i, "");
}
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { mergeWarriors, newWarrior, validateWarriors } from "./bots";
describe("bot roster", () => {
it("merges imported bots by case-insensitive filename", () => {
expect(mergeWarriors(
[{ name: "one.x86-32.asm", source: "old" }],
[{ name: "ONE.x86-32.asm", source: "updated" }, { name: "two.arm-32.asm", source: "new" }],
)).toEqual([
{ name: "ONE.x86-32.asm", source: "updated" },
{ name: "two.arm-32.asm", source: "new" },
]);
});
it("creates unique editable x86 bot names", () => {
expect(newWarrior([{ name: "new-warrior-1.x86-32.asm", source: "nop" }]).name)
.toBe("new-warrior-2.x86-32.asm");
});
it("validates count, filenames, duplicates, and source", () => {
expect(() => validateWarriors([{ name: "one.x86-32.asm", source: "nop" }])).toThrow(/at least two/i);
expect(() => validateWarriors([
{ name: "one.x86-32.asm", source: "nop" },
{ name: "two.asm", source: "nop" },
])).toThrow(/architecture/i);
expect(() => validateWarriors([
{ name: "one.x86-32.asm", source: "nop" },
{ name: "ONE.x86-32.asm", source: "nop" },
])).toThrow(/duplicate/i);
expect(() => validateWarriors([
{ name: "one.x86-32.asm", source: "nop" },
{ name: "two.arm-32.asm", source: "" },
])).toThrow(/source is empty/i);
});
});
+40
View File
@@ -0,0 +1,40 @@
import { architectureFromFilename } from "./architecture";
import type { WarriorSource } from "./protocol";
export function cloneWarriors(warriors: WarriorSource[]): WarriorSource[] {
return warriors.map((warrior) => ({ ...warrior }));
}
export function mergeWarriors(current: WarriorSource[], incoming: WarriorSource[]): WarriorSource[] {
const merged = cloneWarriors(current);
for (const warrior of incoming) {
const index = merged.findIndex((entry) => entry.name.toLowerCase() === warrior.name.toLowerCase());
if (index >= 0) merged[index] = { ...warrior };
else merged.push({ ...warrior });
}
return merged;
}
export function newWarrior(warriors: WarriorSource[]): WarriorSource {
const names = new Set(warriors.map((warrior) => warrior.name.toLowerCase()));
let suffix = 1;
while (names.has(`new-warrior-${suffix}.x86-32.asm`)) suffix++;
return {
name: `new-warrior-${suffix}.x86-32.asm`,
source: "start:\n nop\n jmp start\n",
};
}
export function validateWarriors(warriors: WarriorSource[]): void {
if (warriors.length < 2) throw new Error("Add at least two bots before starting a tournament.");
const names = new Set<string>();
for (const warrior of warriors) {
const name = warrior.name.trim();
if (!name.toLowerCase().endsWith(".asm")) throw new Error(`${name || "Unnamed bot"}: filename must end in .asm`);
architectureFromFilename(name);
const key = name.toLowerCase();
if (names.has(key)) throw new Error(`Duplicate bot filename: ${name}`);
names.add(key);
if (!warrior.source.trim()) throw new Error(`${name}: source is empty`);
}
}
+411
View File
@@ -0,0 +1,411 @@
import { architectureFromFilename, displayName, r2ArchitectureCommands, type Architecture } from "./architecture";
import { emptyMemory, type PlayerView, type WarriorSource } from "./protocol";
import { R2Core, RadareRuntime } from "./radare";
export const ARENA_SIZE = 1024;
export const MAX_WARRIOR_SIZE = 512;
interface MemoryAccess {
address: number;
size: number;
kind: "read" | "write";
}
interface PlayerInfo {
pc: number;
pcSize: number;
oldPc: number;
oldPcSize: number;
instruction: string;
disassembly: string;
registers: string;
cycles: number;
memoryAccess: MemoryAccess[];
dead: boolean;
deathReason: string;
deathInstruction: string;
}
interface Player {
filename: string;
name: string;
source: string;
origin: number;
size: number;
code: string;
info: PlayerInfo;
}
export interface EngineSnapshot {
players: [PlayerView, PlayerView];
memory: string[];
activePlayer: number;
status: string;
}
export interface TickResult {
deadPlayer: number | null;
deathReason: string;
deathInstruction: string;
snapshot: EngineSnapshot;
}
export type RandomSource = () => number;
const baseColors = ["b", "r"] as const;
const readColors = ["q", "y"] as const;
const writeColors = ["v", "o"] as const;
export function parseMemoryAccess(output: string): MemoryAccess[] {
const accesses: MemoryAccess[] = [];
const pattern = /^f mem\.(read|write)\.[^ ]*\s+0x([0-9a-f]+)\s+@\s+0x([0-9a-f]+)/i;
for (const line of output.split("\n")) {
const match = pattern.exec(line.trim());
if (!match) continue;
accesses.push({
kind: match[1].toLowerCase() as "read" | "write",
size: Number.parseInt(match[2], 16),
address: Number.parseInt(match[3], 16),
});
}
return accesses;
}
export function parseCycles(output: string): number {
try {
const operation = JSON.parse(output)[0];
const cycles = Number(operation?.cycles ?? 0);
return Number.isFinite(cycles) && cycles > 0 ? cycles : 0;
} catch {
return 0;
}
}
function parseNumber(output: string): number {
const value = output.trim().split(/\s+/).at(-1) ?? "0";
if (/^-?0x/i.test(value)) return Number.parseInt(value, 16);
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function registersForDisplay(output: string): string {
let registers: string[];
try {
const values = JSON.parse(output) as Record<string, number>;
registers = Object.entries(values)
.filter(([name]) => name !== "oeax")
.map(([name, value]) => `${name} = 0x${Number(value).toString(16).padStart(8, "0")}`);
} catch {
registers = output
.split("\n")
.map((line) => line.trim().replace(/^aer\s+/, ""))
.filter((line) => line && !line.startsWith("oeax"));
}
const rows: string[] = [];
for (let index = 0; index < registers.length; index += 3) {
rows.push(registers.slice(index, index + 3).join(" "));
}
return rows.join("\n");
}
function codeForDisplay(info: PlayerInfo): string {
const lines = info.disassembly.trimEnd().split("\n");
const highlighted = lines.map((line) => {
const escaped = escapeHtml(line);
const address = line.trimStart().match(/^(0x[0-9a-f]+)/i)?.[1];
return address && parseNumber(address) === info.pc ? `<span class="s">${escaped}</span>` : escaped;
});
return `Cycles:${info.cycles}\nActual Instruction:\n ${escapeHtml(info.instruction)}\n${highlighted.join("\n")}`;
}
function emptyInfo(): PlayerInfo {
return {
pc: -1,
pcSize: 0,
oldPc: -1,
oldPcSize: 0,
instruction: "",
disassembly: "",
registers: "",
cycles: 0,
memoryAccess: [],
dead: false,
deathReason: "",
deathInstruction: "",
};
}
export class CombatEngine {
private cores: [R2Core, R2Core] | null = null;
private players: [Player, Player] | null = null;
private memory = emptyMemory();
private currentPlayer = 0;
private status = "Idle";
constructor(private runtime: RadareRuntime, private random: RandomSource = Math.random) {}
initialize(warriors: [WarriorSource, WarriorSource]): EngineSnapshot {
this.disposeCores();
const assembled = warriors.map((warrior) => {
const architecture = architectureFromFilename(warrior.name);
const code = this.runtime.assemble(architecture, warrior.source);
const size = code.length / 2;
if (size > MAX_WARRIOR_SIZE) throw new Error(`${warrior.name}: warrior is ${size} bytes; the limit is ${MAX_WARRIOR_SIZE}`);
return { warrior, architecture, code, size };
}) as [
{ warrior: WarriorSource; architecture: Architecture; code: string; size: number },
{ warrior: WarriorSource; architecture: Architecture; code: string; size: number },
];
const origins = this.pickOrigins(assembled[0].size, assembled[1].size);
this.cores = [this.runtime.createCore(), this.runtime.createCore()];
const players = assembled.map((entry, index) => {
const core = this.cores![index];
core.cmd([
`o+ malloc://${ARENA_SIZE}`,
r2ArchitectureCommands(entry.architecture),
"e scr.color=false",
"e asm.lines=false",
"e asm.flags=false",
"e asm.comments=false",
"e asm.bytes=false",
"e cfg.r2wars=true",
"aei",
"aeim",
`w0 ${ARENA_SIZE} @ 0`,
`wx ${entry.code} @ ${origins[index]}`,
`aer PC=${origins[index]}`,
`aer SP=SP+${origins[index]}`,
"e cmd.esil.todo=f theend=1",
"e cmd.esil.trap=f theend=2",
"e cmd.esil.intr=f theend=3",
"e cmd.esil.ioer=f theend=4",
"f theend=0",
`b ${ARENA_SIZE}`,
].join(";"));
return {
filename: entry.warrior.name,
name: displayName(entry.warrior.name),
source: entry.warrior.source,
origin: origins[index],
size: entry.size,
code: entry.code,
info: emptyInfo(),
};
}) as [Player, Player];
for (let coreIndex = 0; coreIndex < 2; coreIndex++) {
for (let playerIndex = 0; playerIndex < 2; playerIndex++) {
if (coreIndex === playerIndex) continue;
this.cores[coreIndex].cmd(`wx ${players[playerIndex].code} @ ${players[playerIndex].origin}`);
}
}
this.players = players;
this.currentPlayer = 0;
this.status = "Paused — ready to step";
this.resetMarkers();
this.players[0].info = this.readInfo(0);
this.players[1].info = this.readInfo(1);
this.paintPc(0);
this.paintPc(1);
return this.snapshot();
}
resetRound(): EngineSnapshot {
if (!this.players) throw new Error("No combat is loaded");
const warriors: [WarriorSource, WarriorSource] = [
{ name: this.players[0].filename, source: this.players[0].source },
{ name: this.players[1].filename, source: this.players[1].source },
];
return this.initialize(warriors);
}
tick(): TickResult {
if (!this.players || !this.cores) throw new Error("No combat is loaded");
const playerIndex = this.currentPlayer;
const player = this.players[playerIndex];
let deadPlayer: number | null = null;
if (player.info.cycles === 0) {
this.executeInstruction(playerIndex);
if (player.info.dead) deadPlayer = playerIndex;
} else {
player.info.cycles--;
}
this.status = player.info.dead ? player.info.deathReason : "Running";
if (!player.info.dead) {
this.paintOldPc(playerIndex);
this.paintAccesses(playerIndex, player.info.memoryAccess);
this.paintPc(playerIndex);
this.paintPc(1 - playerIndex);
}
this.currentPlayer = 1 - this.currentPlayer;
return {
deadPlayer,
deathReason: player.info.deathReason,
deathInstruction: player.info.deathInstruction,
snapshot: this.snapshot(),
};
}
setStatus(status: string): void {
this.status = status;
}
snapshot(): EngineSnapshot {
if (!this.players) {
return {
players: [
{ name: "Player 1", regs: "", code: "" },
{ name: "Player 2", regs: "", code: "" },
],
memory: [...this.memory],
activePlayer: -1,
status: this.status,
};
}
return {
players: this.players.map((player) => ({
name: player.name,
regs: player.info.registers,
code: codeForDisplay(player.info),
})) as [PlayerView, PlayerView],
memory: [...this.memory],
activePlayer: this.currentPlayer,
status: this.status,
};
}
names(): [string, string] {
if (!this.players) return ["Player 1", "Player 2"];
return [this.players[0].name, this.players[1].name];
}
dispose(): void {
this.disposeCores();
this.players = null;
}
private executeInstruction(playerIndex: number): void {
const core = this.cores![playerIndex];
const other = this.cores![1 - playerIndex];
const info = this.players![playerIndex].info;
const executedInstruction = core.cmd("pd 1 @r:PC").replace(/\r/g, "").trim();
const accessOutput = core.cmd("f-mem.*;aea*@r:PC").replace(/\r/g, "");
const accesses = parseMemoryAccess(accessOutput);
const oldPc = info.pc;
const oldPcSize = info.pcSize;
core.cmd("aes");
for (const access of accesses) {
if (access.kind !== "write" || access.size <= 0 || access.address < 0 || access.address >= ARENA_SIZE) continue;
const available = Math.min(access.size, ARENA_SIZE - access.address);
const bytes = core.cmd(`p8 ${available} @ ${access.address}`).trim();
if (/^[0-9a-f]+$/i.test(bytes)) other.cmd(`wx ${bytes} @ ${access.address}`);
}
const pc = parseNumber(core.cmd("aer PC"));
const trap = core.cmd("?v 1+theend").trim();
let deathReason = "";
if (executedInstruction.includes("unaligned")) deathReason = "Executed unaligned instruction";
else if (executedInstruction.includes("invalid")) deathReason = "Executed invalid instruction";
else if (pc < 0 || pc >= ARENA_SIZE) deathReason = "Instruction executed out of the arena";
else if (!["0", "1", "0x0", "0x1", "0x00", "0x01"].includes(trap)) {
const trapValue = parseNumber(trap);
deathReason = trapValue === 5
? "Instruction read/write outside the arena"
: trapValue === 4
? "Syscall or interrupt instruction executed"
: `ESIL stopped execution (trap ${trap || "unknown"})`;
}
const next = this.readInfo(playerIndex);
next.oldPc = oldPc;
next.oldPcSize = oldPcSize;
next.memoryAccess = accesses;
next.dead = Boolean(deathReason);
next.deathReason = deathReason;
next.deathInstruction = executedInstruction;
this.players![playerIndex].info = next;
this.players![1 - playerIndex].info.disassembly = other.cmd("pd 8 @r:PC").replace(/\r/g, "");
}
private readInfo(playerIndex: number): PlayerInfo {
const core = this.cores![playerIndex];
const pc = parseNumber(core.cmd("aer PC"));
const registerOutput = core.cmd("aerj").replace(/\r/g, "");
return {
...emptyInfo(),
pc,
pcSize: parseNumber(core.cmd("?v $l@r:PC")),
instruction: core.cmd("pd 1 @r:PC").replace(/\r/g, "").trim(),
disassembly: core.cmd("pd 8 @r:PC").replace(/\r/g, ""),
registers: registersForDisplay(registerOutput),
cycles: parseCycles(core.cmd("aoj 1 @r:PC").replace(/\r/g, "")),
memoryAccess: parseMemoryAccess(core.cmd("f-mem.*;aea*@r:PC").replace(/\r/g, "")),
};
}
private pickOrigins(firstSize: number, secondSize: number): [number, number] {
const lowerFirst = this.random() < 0.5;
const aligned = (start: number, end: number): number => {
const slots = Math.max(1, Math.floor((end - start) / 4) + 1);
return start + Math.floor(this.random() * slots) * 4;
};
const lower = (size: number) => aligned(0, Math.max(0, 512 - size));
const upper = (size: number) => aligned(512, Math.max(512, ARENA_SIZE - size));
return lowerFirst ? [lower(firstSize), upper(secondSize)] : [upper(firstSize), lower(secondSize)];
}
private resetMarkers(): void {
this.memory = emptyMemory();
if (!this.players) return;
for (let playerIndex = 0; playerIndex < 2; playerIndex++) {
const player = this.players[playerIndex];
this.paint(player.origin, player.size, baseColors[playerIndex]);
}
}
private paintOldPc(playerIndex: number): void {
const info = this.players![playerIndex].info;
this.paint(info.oldPc, info.oldPcSize, baseColors[playerIndex]);
}
private paintPc(playerIndex: number): void {
const info = this.players![playerIndex].info;
this.paint(info.pc, Math.max(1, info.pcSize), `${baseColors[playerIndex]}X`);
}
private paintAccesses(playerIndex: number, accesses: MemoryAccess[]): void {
for (const access of accesses) {
const color = access.kind === "read" ? readColors[playerIndex] : writeColors[playerIndex];
this.paint(access.address, access.size, `${color}${access.kind === "read" ? "R" : "W"}`);
}
}
private paint(offset: number, count: number, value: string): void {
for (let index = 0; index < count; index++) {
const address = offset + index;
if (address >= 0 && address < ARENA_SIZE) this.memory[address] = value;
}
}
private disposeCores(): void {
if (!this.cores) return;
this.cores[0].dispose();
this.cores[1].dispose();
this.cores = null;
}
}
+723
View File
@@ -0,0 +1,723 @@
import { emptyState, type AppState, type MainToWorker, type WarriorSource, type WorkerToMain } from "./protocol";
import { cloneWarriors, mergeWarriors, newWarrior, validateWarriors } from "./bots";
const dotnetMode = import.meta.env.VITE_R2WARS_ENGINE === "dotnet"
|| document.documentElement.dataset.r2warsEngine === "dotnet";
let worker: Worker | null = null;
let socket: WebSocket | null = null;
let reconnectTimer = 0;
let initialBotsResolve: ((warriors: WarriorSource[]) => void) | null = null;
let initialBotsReject: ((error: Error) => void) | null = null;
let initialized = false;
let state = emptyState();
let ready = false;
let actionPending = false;
let bots: WarriorSource[] = [];
let selectedBot = -1;
let botsDirty = false;
let fileMode: "replace" | "merge" = "replace";
let latestAssemblyRequest = 0;
const element = <T extends HTMLElement>(id: string): T => {
const value = document.getElementById(id);
if (!value) throw new Error(`Missing UI element #${id}`);
return value as T;
};
function send(message: MainToWorker): void {
if (!dotnetMode) {
worker?.postMessage(message);
return;
}
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error("The .NET connection is not ready");
socket.send(message.type === "command" ? message.command : JSON.stringify(message));
}
function sendCommand(command: string): void {
if (!ready) return;
actionPending = true;
send({ type: "command", command });
syncControls();
}
function installLocalControls(): HTMLInputElement {
document.title = `r2wars — ${dotnetMode ? ".NET" : "WebAssembly"} arena`;
const description = document.createElement("meta");
description.name = "description";
description.content = dotnetMode
? "Run multi-architecture r2wars tournaments with the native .NET and radare2 engine."
: "Run multi-architecture r2wars tournaments entirely in your browser with radare2 WebAssembly.";
document.head.append(description);
const style = document.createElement("style");
style.textContent = `
.connection.local { background:#d8e8ff; color:#174f85; }
.drop-active::after { content:'Drop .asm warriors to load them'; position:fixed; inset:18px; z-index:10; display:grid; place-items:center; border:4px dashed springgreen; background:rgba(0,0,0,.88); color:springgreen; font:700 1.5rem monospace; }
.wasm-note { color:#444; font-size:.72em; margin-right:auto; }
#report_download { margin-left:8px; position:absolute; right:100px; top:24px; width:auto; padding:7px 18px; }
.control-row { grid-template-columns:repeat(3,1fr); }
.bot-manager[hidden] { display:none; }
.bot-manager { position:fixed; inset:2%; z-index:20; display:grid; place-items:center; background:rgba(0,0,0,.84); border:3px solid #999; border-radius:7px; }
.bot-manager-panel { box-sizing:border-box; display:flex; flex-direction:column; width:min(1100px,96%); height:min(760px,94%); overflow:hidden; color:#111; background:#ddd; border:2px solid #eee; box-shadow:0 12px 42px #000; }
.bot-manager-header { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:10px 14px; color:white; background:linear-gradient(90deg,#123b8d,#8b1717); }
.bot-manager-header h2 { margin:0; font-size:1.15rem; }
.bot-manager-header span { font-size:.78rem; }
.bot-manager-body { min-height:0; flex:1; display:grid; grid-template-columns:minmax(220px,30%) 1fr; gap:10px; padding:10px; }
.bot-sidebar,.bot-editor { min-height:0; display:flex; flex-direction:column; gap:8px; }
.bot-editor.no-selection > :not(.bot-editor-empty) { display:none; }
.bot-list { min-height:120px; flex:1; overflow:auto; padding:4px; background:#171717; border:1px inset #aaa; }
.bot-list button { margin-bottom:4px; padding:7px 8px; overflow:hidden; text-align:left; text-overflow:ellipsis; white-space:nowrap; color:#e8e8e8; background:#292929; border:1px solid #555; }
.bot-list button.selected { color:#111; background:#9ed1ff; border-color:#3275aa; font-weight:bold; }
.bot-sidebar-actions,.bot-manager-footer { display:flex; gap:8px; }
.bot-sidebar-actions button,.bot-manager-footer button { width:auto; padding:7px 14px; }
.bot-editor label { font-size:.8rem; font-weight:bold; }
.bot-editor input,.bot-editor textarea { box-sizing:border-box; width:100%; color:#d8ffd8; background:#111; border:1px inset #aaa; font:14px/1.4 Consolas,'Courier New',monospace; }
.bot-editor input { padding:7px; }
.bot-editor textarea { min-height:180px; flex:1; padding:10px; resize:none; tab-size:2; }
.bot-assembly { min-height:84px; max-height:150px; display:flex; flex-direction:column; gap:5px; overflow:auto; padding:7px; color:#ddd; background:#171717; border:1px inset #aaa; }
.bot-assembly-summary { display:flex; align-items:center; gap:7px; font-size:.78rem; }
.bot-assembly-summary strong { min-width:0; overflow-wrap:anywhere; }
.bot-compile-indicator { width:11px; height:11px; flex:0 0 11px; border-radius:50%; background:#777; box-shadow:0 0 5px #555; }
.bot-compile-indicator.pending { background:#e5b72f; box-shadow:0 0 6px #e5b72f; }
.bot-compile-indicator.valid { background:#39e75f; box-shadow:0 0 6px #39e75f; }
.bot-compile-indicator.invalid { background:#f04444; box-shadow:0 0 6px #f04444; }
.bot-assembly-size { margin-left:auto; color:#9ed1ff; white-space:nowrap; }
.bot-byte-array { min-height:38px; margin:0; overflow:auto; color:#b7e6ff; font:12px/1.35 Consolas,'Courier New',monospace; white-space:pre-wrap; word-break:break-all; }
.bot-editor-empty { display:grid; flex:1; place-items:center; color:#555; border:1px dashed #999; }
.bot-manager-footer { align-items:center; padding:9px 12px; border-top:1px solid #999; }
.bot-manager-status { min-height:1.2em; flex:1; color:#444; font-size:.78rem; }
.bot-manager-status.error { color:#8c1010; font-weight:bold; }
#bot_start { background:#d8f5de; border-color:#318244; font-weight:bold; }
@media (max-width: 900px), (orientation: portrait) {
.bot-manager { inset:0; border:0; }
.bot-manager-panel { height:100%; width:100%; }
.bot-manager-body { grid-template-columns:1fr; grid-template-rows:minmax(150px,32%) 1fr; }
.bot-manager-header span { display:none; }
}
`;
document.head.append(style);
const input = document.createElement("input");
input.type = "file";
input.accept = ".asm,text/plain";
input.multiple = true;
input.hidden = true;
input.setAttribute("aria-label", "Choose warrior assembly files");
document.body.append(input);
const inspection = document.querySelector(".inspection-row");
if (inspection) {
const note = document.createElement("span");
note.className = "wasm-note";
note.textContent = `${dotnetMode ? "Native engine" : "Local Wasm"} · drop .asm files anywhere`;
inspection.prepend(note);
}
const manageButton = document.createElement("button");
manageButton.id = "cmd_bots";
manageButton.textContent = "Bots";
manageButton.title = "Add, remove, or edit bots for the next tournament";
document.querySelector(".control-row")?.append(manageButton);
const manager = document.createElement("div");
manager.id = "bot_manager";
manager.className = "bot-manager";
manager.hidden = true;
manager.setAttribute("role", "dialog");
manager.setAttribute("aria-modal", "true");
manager.setAttribute("aria-labelledby", "bot_manager_title");
manager.innerHTML = `
<div class="bot-manager-panel">
<header class="bot-manager-header">
<h2 id="bot_manager_title">Bot manager</h2>
<span>Draft changes are loaded only when a new tournament starts</span>
</header>
<div class="bot-manager-body">
<aside class="bot-sidebar">
<strong id="bot_count">0 bots</strong>
<div id="bot_list" class="bot-list" role="listbox" aria-label="Loaded bots"></div>
<div class="bot-sidebar-actions">
<button id="bot_add" type="button">New bot</button>
<button id="bot_import" type="button">Import .asm</button>
</div>
</aside>
<section class="bot-editor">
<label for="bot_name">Filename and architecture</label>
<input id="bot_name" type="text" autocomplete="off" spellcheck="false" placeholder="name.x86-32.asm">
<label for="bot_source">Assembly source</label>
<textarea id="bot_source" spellcheck="false" aria-label="Bot assembly source"></textarea>
<div class="bot-assembly" aria-live="polite">
<div class="bot-assembly-summary">
<span id="bot_compile_indicator" class="bot-compile-indicator" aria-hidden="true"></span>
<strong id="bot_compile_status">Not checked</strong>
<span id="bot_assembly_size" class="bot-assembly-size">0 bytes</span>
</div>
<pre id="bot_byte_array" class="bot-byte-array">[]</pre>
</div>
<div class="bot-sidebar-actions">
<button id="bot_delete" type="button">Delete selected bot</button>
</div>
<div id="bot_editor_empty" class="bot-editor-empty" hidden>Select a bot to edit its source.</div>
</section>
</div>
<footer class="bot-manager-footer">
<span id="bot_manager_status" class="bot-manager-status" role="status"></span>
<button id="bot_close" type="button">Close</button>
<button id="bot_start" type="button">Start new tournament</button>
</footer>
</div>`;
document.body.append(manager);
const reportButton = document.createElement("button");
reportButton.id = "report_download";
reportButton.textContent = "Download report";
reportButton.hidden = true;
element("overlay").append(reportButton);
reportButton.addEventListener("click", downloadReport);
return input;
}
const fileInput = installLocalControls();
fileInput.addEventListener("change", async () => {
if (fileInput.files?.length) {
const imported = await readFiles(Array.from(fileInput.files));
bots = fileMode === "replace" ? imported : mergeWarriors(bots, imported);
const lastImported = imported.at(-1)?.name.toLowerCase();
selectedBot = lastImported ? bots.findIndex((bot) => bot.name.toLowerCase() === lastImported) : -1;
botsDirty = true;
showBotManager();
}
fileInput.value = "";
});
element("cmd_load").addEventListener("click", () => {
fileMode = "replace";
fileInput.click();
});
element("cmd_bots").addEventListener("click", showBotManager);
element("cmd_run").addEventListener("click", () => {
if (state.workflow === "running") sendCommand("cmd_stop");
else if (state.workflow === "paused") sendCommand("cmd_run");
else startNewTournament();
});
element("cmd_step").addEventListener("click", () => sendCommand("cmd_step"));
element("cmd_reset").addEventListener("click", () => {
const active = state.workflow === "running" || state.workflow === "paused";
if (!active || window.confirm("Reset this tournament? Current progress and scores will be cleared.")) prepareNewTournament();
});
element("cmd_scores").addEventListener("click", showScores);
element("log_prev").addEventListener("click", () => sendCommand("cmd_prevlog"));
element("log_next").addEventListener("click", () => sendCommand("cmd_nextlog"));
element("score_close").addEventListener("click", hideScores);
element("nowarriorsclose").addEventListener("click", hideNoWarriorsDialog);
element("bot_add").addEventListener("click", addBot);
element("bot_import").addEventListener("click", () => {
fileMode = "merge";
fileInput.click();
});
element("bot_delete").addEventListener("click", deleteSelectedBot);
element("bot_close").addEventListener("click", hideBotManager);
element("bot_start").addEventListener("click", startNewTournament);
element<HTMLInputElement>("bot_name").addEventListener("input", (event) => {
if (selectedBot < 0) return;
bots[selectedBot].name = (event.currentTarget as HTMLInputElement).value;
botsDirty = true;
renderBotList();
updateBotManagerStatus();
syncControls();
requestBotAssembly();
});
element<HTMLTextAreaElement>("bot_source").addEventListener("input", (event) => {
if (selectedBot < 0) return;
bots[selectedBot].source = (event.currentTarget as HTMLTextAreaElement).value;
botsDirty = true;
updateBotManagerStatus();
requestBotAssembly();
});
element("overlay").addEventListener("click", (event) => {
if (event.target === element("overlay")) hideScores();
});
element("bot_manager").addEventListener("click", (event) => {
if (event.target === element("bot_manager")) hideBotManager();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (!element<HTMLDivElement>("bot_manager").hidden) hideBotManager();
else {
hideScores();
hideNoWarriorsDialog();
}
}
});
document.addEventListener("dragover", (event) => {
event.preventDefault();
if (state.workflow !== "running") document.body.classList.add("drop-active");
});
document.addEventListener("dragleave", (event) => {
if (!event.relatedTarget) document.body.classList.remove("drop-active");
});
document.addEventListener("drop", async (event) => {
event.preventDefault();
document.body.classList.remove("drop-active");
if (state.workflow === "running") return;
const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.name.toLowerCase().endsWith(".asm"));
if (files.length) {
const imported = await readFiles(files);
bots = mergeWarriors(bots, imported);
const lastImported = imported.at(-1)?.name.toLowerCase();
selectedBot = lastImported ? bots.findIndex((bot) => bot.name.toLowerCase() === lastImported) : -1;
botsDirty = true;
showBotManager();
}
});
async function handleTransportMessage(message: WorkerToMain): Promise<void> {
if (message.type === "ready") {
ready = true;
element("connection").textContent = dotnetMode ? ".NET server" : "Local Wasm";
element("connection").className = "connection local";
if (!initialized) {
initialized = true;
await loadExamples();
} else if (dotnetMode) {
send({ type: "command", command: "cmd_state" });
}
} else if (message.type === "bots") {
initialBotsResolve?.(message.warriors);
initialBotsResolve = null;
initialBotsReject = null;
} else if (message.type === "assembly") {
showAssemblyResult(message);
} else if (message.type === "state") {
actionPending = false;
state = mergeAppState(state, message.state);
updateUI();
} else {
if (message.recoverable) {
actionPending = false;
initialBotsReject?.(new Error(message.message));
initialBotsResolve = null;
initialBotsReject = null;
if (initialized) {
showBotManager();
updateBotManagerStatus(message.message);
}
syncControls();
} else {
showFatal(message.message);
}
}
}
function mergeAppState(current: AppState, update: Partial<AppState>): AppState {
return {
...current,
...update,
player1: { ...current.player1, ...update.player1 },
player2: { ...current.player2, ...update.player2 },
memory: update.memory ?? current.memory,
};
}
async function readFiles(files: File[]): Promise<WarriorSource[]> {
return Promise.all(files.map(async (file) => ({ name: file.name, source: await file.text() })));
}
async function loadExamples(): Promise<void> {
try {
const warriors = dotnetMode ? await requestDotnetBots() : await fetchBundledBots();
bots = cloneWarriors(warriors);
selectedBot = bots.length ? 0 : -1;
botsDirty = false;
send({ type: "load", warriors: cloneWarriors(bots) });
} catch (error) {
state.workflow = "idle";
state.message = `Choose at least two warrior files. Bundled examples could not be loaded: ${error instanceof Error ? error.message : error}`;
updateUI();
}
}
async function fetchBundledBots(): Promise<WarriorSource[]> {
const base = import.meta.env.BASE_URL;
const manifestResponse = await fetch(`${base}warriors/manifest.json`);
if (!manifestResponse.ok) throw new Error(`HTTP ${manifestResponse.status}`);
const names = await manifestResponse.json() as string[];
return Promise.all(names.map(async (name) => {
const response = await fetch(`${base}warriors/${encodeURIComponent(name)}`);
if (!response.ok) throw new Error(`${name}: HTTP ${response.status}`);
return { name, source: await response.text() };
}));
}
function requestDotnetBots(): Promise<WarriorSource[]> {
return new Promise((resolve, reject) => {
initialBotsResolve = resolve;
initialBotsReject = reject;
socket?.send(JSON.stringify({ type: "bots" }));
window.setTimeout(() => {
if (initialBotsResolve !== resolve) return;
initialBotsResolve = null;
initialBotsReject = null;
reject(new Error("Timed out while loading bots from the .NET server"));
}, 5000);
});
}
function preparedBots(): WarriorSource[] {
return bots.map((bot) => ({ name: bot.name.trim(), source: bot.source }));
}
function startNewTournament(): void {
try {
const warriors = preparedBots();
validateWarriors(warriors);
bots = cloneWarriors(warriors);
botsDirty = false;
actionPending = true;
hideBotManager();
send({ type: "start", warriors: cloneWarriors(bots) });
syncControls();
} catch (error) {
showBotManager();
updateBotManagerStatus(error instanceof Error ? error.message : String(error));
}
}
function prepareNewTournament(): void {
try {
const warriors = preparedBots();
validateWarriors(warriors);
bots = cloneWarriors(warriors);
botsDirty = false;
actionPending = true;
send({ type: "load", warriors: cloneWarriors(bots) });
syncControls();
} catch (error) {
showBotManager();
updateBotManagerStatus(error instanceof Error ? error.message : String(error));
}
}
function showBotManager(): void {
if (!ready || state.workflow === "running") return;
const manager = element<HTMLDivElement>("bot_manager");
manager.hidden = false;
renderBotManager();
if (selectedBot >= 0) element<HTMLInputElement>("bot_name").focus();
else element<HTMLButtonElement>("bot_add").focus();
}
function hideBotManager(): void {
element<HTMLDivElement>("bot_manager").hidden = true;
element<HTMLButtonElement>("cmd_bots").focus({ preventScroll: true });
}
function addBot(): void {
bots.push(newWarrior(bots));
selectedBot = bots.length - 1;
botsDirty = true;
renderBotManager();
const name = element<HTMLInputElement>("bot_name");
name.focus();
name.select();
}
function deleteSelectedBot(): void {
if (selectedBot < 0) return;
const name = bots[selectedBot].name || "this bot";
if (!window.confirm(`Delete ${name} from the next tournament?`)) return;
bots.splice(selectedBot, 1);
selectedBot = bots.length ? Math.min(selectedBot, bots.length - 1) : -1;
botsDirty = true;
renderBotManager();
}
function selectBot(index: number): void {
selectedBot = index;
renderBotManager();
element<HTMLTextAreaElement>("bot_source").focus();
}
function renderBotList(): void {
const list = element<HTMLDivElement>("bot_list");
list.replaceChildren();
bots.forEach((bot, index) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = bot.name || "Unnamed bot";
button.title = bot.name || "Unnamed bot";
button.classList.toggle("selected", index === selectedBot);
button.setAttribute("role", "option");
button.setAttribute("aria-selected", String(index === selectedBot));
button.addEventListener("click", () => selectBot(index));
list.append(button);
});
element("bot_count").textContent = `${bots.length} bot${bots.length === 1 ? "" : "s"}`;
element<HTMLButtonElement>("cmd_bots").textContent = `Bots (${bots.length})`;
}
function renderBotManager(): void {
if (selectedBot >= bots.length) selectedBot = bots.length - 1;
renderBotList();
const selected = selectedBot >= 0 ? bots[selectedBot] : undefined;
const editor = document.querySelector<HTMLElement>(".bot-editor");
editor?.classList.toggle("no-selection", !selected);
element<HTMLDivElement>("bot_editor_empty").hidden = Boolean(selected);
const name = element<HTMLInputElement>("bot_name");
const source = element<HTMLTextAreaElement>("bot_source");
name.value = selected?.name ?? "";
source.value = selected?.source ?? "";
name.disabled = !selected;
source.disabled = !selected;
element<HTMLButtonElement>("bot_delete").disabled = !selected;
element<HTMLButtonElement>("bot_start").disabled = bots.length < 2 || actionPending;
updateBotManagerStatus();
requestBotAssembly();
}
function updateBotManagerStatus(error = ""): void {
const status = element("bot_manager_status");
status.classList.toggle("error", Boolean(error));
status.textContent = error || (botsDirty
? "Draft changed — the active tournament is untouched."
: "This roster matches the most recently loaded tournament.");
}
function requestBotAssembly(): void {
const requestId = ++latestAssemblyRequest;
const selected = selectedBot >= 0 ? bots[selectedBot] : undefined;
const indicator = element("bot_compile_indicator");
indicator.className = `bot-compile-indicator${selected ? " pending" : ""}`;
element("bot_compile_status").textContent = selected ? "Checking…" : "Not checked";
element("bot_assembly_size").textContent = "0 bytes";
element("bot_byte_array").textContent = "[]";
if (!selected || !ready) return;
send({ type: "assemble", requestId, warrior: { ...selected } });
}
function showAssemblyResult(result: Extract<WorkerToMain, { type: "assembly" }>): void {
if (result.requestId !== latestAssemblyRequest) return;
const indicator = element("bot_compile_indicator");
indicator.className = `bot-compile-indicator ${result.ok ? "valid" : "invalid"}`;
element("bot_compile_status").textContent = result.message;
element("bot_assembly_size").textContent = `${result.size} byte${result.size === 1 ? "" : "s"}`;
element("bot_byte_array").textContent = result.bytes.length
? `[${result.bytes.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(", ")}]`
: "[]";
}
function showFatal(message: string): void {
ready = false;
actionPending = false;
state.workflow = "error";
state.message = message;
state.status = dotnetMode ? ".NET engine error" : "WebAssembly error";
element("connection").textContent = "Engine error";
element("connection").className = "connection disconnected";
updateUI();
}
function connectTransport(): void {
if (dotnetMode) {
connectDotnet();
return;
}
worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
worker.onmessage = (event: MessageEvent<WorkerToMain>) => void handleTransportMessage(event.data);
worker.onerror = (event) => showFatal(event.message || "The WebAssembly worker stopped unexpectedly");
const wasmUrl = new URL(`${import.meta.env.BASE_URL}radare2.wasm`, window.location.href).href;
worker.postMessage({ type: "init", wasmUrl } satisfies MainToWorker);
}
function connectDotnet(): void {
window.clearTimeout(reconnectTimer);
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
const url = window.R2WARS_WS_URL || `${scheme}://${window.location.hostname}:9966/r2wars`;
socket = new WebSocket(url);
element("connection").textContent = "Connecting…";
element("connection").className = "connection connecting";
socket.onopen = () => void handleTransportMessage({ type: "ready" });
socket.onmessage = (event) => handleDotnetPayload(String(event.data));
socket.onerror = () => {
element("connection").textContent = "Connection error";
element("connection").className = "connection disconnected";
};
socket.onclose = () => {
ready = false;
actionPending = false;
initialBotsReject?.(new Error("The .NET connection closed while loading bots"));
initialBotsResolve = null;
initialBotsReject = null;
element("connection").textContent = "Disconnected — reconnecting…";
element("connection").className = "connection disconnected";
syncControls();
reconnectTimer = window.setTimeout(connectDotnet, 2000);
};
}
function handleDotnetPayload(payload: string): void {
if (payload === "none") return;
if (payload === "on") {
send({ type: "command", command: "moreflow" });
return;
}
if (payload === "nowarriors") {
initialBotsResolve?.([]);
initialBotsResolve = null;
initialBotsReject = null;
return;
}
try {
const parsed = JSON.parse(payload) as Record<string, unknown>;
if (parsed.type === "bots" || parsed.type === "assembly" || parsed.type === "error") {
void handleTransportMessage(parsed as unknown as WorkerToMain);
} else {
void handleTransportMessage({ type: "state", state: parsed as Partial<AppState> });
}
} catch {
showFatal("The .NET server sent an invalid update");
}
}
function syncControls(): void {
const available = ready && !actionPending;
const running = state.workflow === "running";
const paused = state.workflow === "paused";
const readyToRun = state.workflow === "ready";
const finished = state.workflow === "finished";
const load = element<HTMLButtonElement>("cmd_load");
load.disabled = !ready || running;
load.textContent = state.workflow === "loading" ? "Loading engine…" : "Choose warriors";
load.title = "Choose two or more architecture-tagged .asm warrior files";
const run = element<HTMLButtonElement>("cmd_run");
run.disabled = !available || (!readyToRun && !paused && !running);
run.textContent = running ? "Pause" : paused ? "Resume" : "Start tournament";
run.className = running ? "primary pause" : "primary";
element<HTMLButtonElement>("cmd_step").disabled = !available || !paused;
element<HTMLButtonElement>("cmd_reset").disabled = !available || state.workflow === "idle" || state.workflow === "loading";
element<HTMLButtonElement>("cmd_reset").textContent = finished ? "Play again" : "Reset";
element<HTMLButtonElement>("cmd_scores").disabled = !ready || state.totalCombats === 0;
element<HTMLButtonElement>("cmd_scores").textContent = finished ? "Final scores" : "Scores";
const manage = element<HTMLButtonElement>("cmd_bots");
manage.disabled = !available || running;
manage.textContent = `Bots (${bots.length})`;
element<HTMLButtonElement>("log_prev").disabled = !available || !state.canBrowseEarlier || (!paused && !finished);
element<HTMLButtonElement>("log_next").disabled = !available || !state.canBrowseLater || (!paused && !finished);
element<HTMLButtonElement>("report_download").hidden = !state.report;
if (running) element<HTMLDivElement>("bot_manager").hidden = true;
}
function updateUI(): void {
element("memory").innerHTML = memoryWidget();
element("status").textContent = state.status || "";
element("console").textContent = state.console || "";
element("workflow_message").textContent = state.message || "";
element("regs1").textContent = state.player1.regs || "";
element("code1").innerHTML = state.player1.code || "";
element("player1").textContent = state.player1.name || "Player 1";
element("regs2").textContent = state.player2.regs || "";
element("code2").innerHTML = state.player2.code || "";
element("player2").textContent = state.player2.name || "Player 2";
setActivePlayer(state.activePlayer);
const labels: Record<string, string> = {
loading: "LOADING ENGINE",
idle: "NO TOURNAMENT",
ready: "READY TO START",
running: "RUNNING",
paused: "PAUSED",
finished: "TOURNAMENT OVER",
error: "ENGINE ERROR",
};
const titles: Record<string, string> = {
loading: "Starting the local engine",
idle: "Choose warriors",
ready: "Tournament ready",
finished: "Tournament over",
error: "The local engine stopped",
};
element("workflow_badge").textContent = labels[state.workflow] || state.workflow;
element("workflow_badge").className = `workflow-badge ${state.workflow}`;
element("progress").textContent = state.workflow === "idle" || state.workflow === "loading"
? (dotnetMode ? "Connected to the native engine" : "Runs entirely in this browser")
: `${state.completedCombats} / ${state.totalCombats} battles${state.workflow === "finished" ? " complete" : ""}`;
element("stage_title").textContent = titles[state.workflow] || "";
element("stage_detail").textContent = state.message || "";
const notice = element("stage_notice");
notice.className = `stage-notice ${state.workflow}`;
notice.hidden = state.workflow === "running" || state.workflow === "paused";
if (element<HTMLElement>("overlay").style.display === "block") refreshScores();
syncControls();
}
function setActivePlayer(activePlayer: number): void {
for (let index = 0; index < 2; index++) {
const marker = element(`active${index + 1}`);
const active = activePlayer === index;
marker.classList.toggle("active", active);
marker.setAttribute("aria-label", active ? `Player ${index + 1} has the active turn` : `Player ${index + 1} is waiting`);
}
}
function memoryWidget(): string {
let html = '<table class="memtable">';
for (let row = 0; row < 32; row++) {
let cells = "";
for (let column = 0; column < 32; column++) {
const value = state.memory[row * 32 + column] || "";
const marker = value.length > 1 && /^[RWX]$/.test(value[1]) ? value[1] : "&nbsp;";
cells += `<td style="background-color:${memoryColor(value[0])}" class="box">${marker}</td>`;
}
html += `<tr><td class="addr">0x${(row * 32).toString(16).padStart(4, "0")}</td>${cells}</tr>`;
}
return `${html}</table>`;
}
function memoryColor(value = ""): string {
const colors: Record<string, string> = { r: "red", y: "yellow", g: "green", w: "white", n: "black", b: "blue", q: "aqua", o: "orange", v: "violet" };
return colors[value] || "white";
}
function showScores(): void {
refreshScores();
element<HTMLElement>("overlay").style.display = "block";
element<HTMLButtonElement>("score_close").focus();
}
function refreshScores(): void {
element("score_title").textContent = state.workflow === "finished" ? "Final standings" : "Tournament scores";
element("scores").textContent = state.scores || "No scores yet.";
}
function hideScores(): void {
element<HTMLElement>("overlay").style.display = "none";
}
function hideNoWarriorsDialog(): void {
element<HTMLElement>("nowarriorsdlg").style.display = "none";
}
function downloadReport(): void {
if (!state.report) return;
const blob = new Blob([state.report], { type: "text/plain;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${new Date().toISOString().replaceAll(":", "-").slice(0, 19)}.r2wars.txt`;
link.click();
setTimeout(() => URL.revokeObjectURL(link.href), 0);
}
state.memory = Array.from({ length: 1024 }, () => "");
updateUI();
connectTransport();
declare global {
interface Window {
R2WARS_WS_URL?: string;
}
}
+65
View File
@@ -0,0 +1,65 @@
export type Workflow = "loading" | "idle" | "ready" | "running" | "paused" | "finished" | "error";
export interface WarriorSource {
name: string;
source: string;
}
export interface PlayerView {
name: string;
regs: string;
code: string;
}
export interface AppState {
workflow: Workflow;
message: string;
player1: PlayerView;
player2: PlayerView;
memory: string[];
console: string;
status: string;
scores: string;
completedCombats: number;
totalCombats: number;
activePlayer: number;
historyPosition: number;
historyCount: number;
canBrowseEarlier: boolean;
canBrowseLater: boolean;
report?: string;
}
export type MainToWorker =
| { type: "init"; wasmUrl: string }
| { type: "load"; warriors: WarriorSource[] }
| { type: "start"; warriors: WarriorSource[] }
| { type: "assemble"; requestId: number; warrior: WarriorSource }
| { type: "command"; command: string };
export type WorkerToMain =
| { type: "ready" }
| { type: "bots"; warriors: WarriorSource[] }
| { type: "state"; state: Partial<AppState> }
| { type: "assembly"; requestId: number; ok: boolean; bytes: number[]; size: number; message: string }
| { type: "error"; message: string; recoverable?: boolean };
export const emptyMemory = (): string[] => Array.from({ length: 1024 }, () => "");
export const emptyState = (): AppState => ({
workflow: "loading",
message: "Loading radare2 WebAssembly…",
player1: { name: "Player 1", regs: "", code: "" },
player2: { name: "Player 2", regs: "", code: "" },
memory: emptyMemory(),
console: "[r2wars wasm]",
status: "Loading",
scores: "No tournament loaded.",
completedCombats: 0,
totalCombats: 0,
activePlayer: -1,
historyPosition: 0,
historyCount: 0,
canBrowseEarlier: false,
canBrowseLater: false,
});
+38
View File
@@ -0,0 +1,38 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { architectureFromFilename } from "./architecture";
import { CombatEngine } from "./engine";
import { RadareRuntime } from "./radare";
describe("radare2 WASI integration", () => {
it("assembles labels, creates two cores, and advances a combat", async () => {
const runtime = new RadareRuntime();
const wasm = await readFile(new URL("../public/radare2.wasm", import.meta.url));
await runtime.loadBytes(wasm);
const publicDirectory = new URL("../public/warriors/", import.meta.url);
const examples = JSON.parse(await readFile(new URL("manifest.json", publicDirectory), "utf8")) as string[];
for (const name of examples) {
const source = await readFile(new URL(name, publicDirectory), "utf8");
expect(runtime.assemble(architectureFromFilename(name), source).length, name).toBeGreaterThan(0);
}
const engine = new CombatEngine(runtime, () => 0.25);
const snapshot = engine.initialize([
{
name: "left.x86-32.asm",
source: "call getpc\ngetpc:\npop eax\nloop:\nsub eax, 4\nmov dword ptr [eax], 0\njmp loop",
},
{
name: "right.x86-32.asm",
source: "call getpc\ngetpc:\npop edi\nloop:\nadd edi, 4\nmov dword ptr [edi], 0xcccccccc\njmp loop",
},
]);
expect(snapshot.players.map((player) => player.name)).toEqual(["left.x86-32", "right.x86-32"]);
expect(snapshot.memory).toHaveLength(1024);
let result = engine.tick();
for (let tick = 0; tick < 16; tick++) result = engine.tick();
expect(result.snapshot.activePlayer).toBe(1);
expect(result.snapshot.players[0].regs.length).toBeGreaterThan(0);
expect(engine.resetRound().players.map((player) => player.name)).toEqual(["left.x86-32", "right.x86-32"]);
engine.dispose();
}, 30_000);
});
+125
View File
@@ -0,0 +1,125 @@
import { ConsoleStdout, File, OpenFile, WASI } from "@bjorn3/browser_wasi_shim";
import type { Architecture } from "./architecture";
interface R2Exports extends WebAssembly.Exports {
memory: WebAssembly.Memory;
malloc(size: number): number;
free(pointer: number): void;
r_core_new(): number;
r_core_free(core: number): void;
r_core_cmd_str(core: number, command: number): number;
r_main_rasm2(argc: number, argv: number): number;
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export class RadareRuntime {
private exports!: R2Exports;
private stdout = "";
private stderr = "";
async load(url: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) throw new Error(`Cannot load radare2.wasm: HTTP ${response.status}`);
await this.loadBytes(await response.arrayBuffer());
}
async loadBytes(bytes: BufferSource): Promise<void> {
const stdout = new ConsoleStdout((chunk) => {
this.stdout += decoder.decode(chunk, { stream: true });
});
const stderr = new ConsoleStdout((chunk) => {
this.stderr += decoder.decode(chunk, { stream: true });
});
const wasi = new WASI(
[],
[],
[new OpenFile(new File(new Uint8Array())), stdout, stderr],
);
const module = await WebAssembly.compile(bytes);
const instance = await WebAssembly.instantiate(module, {
wasi_snapshot_preview1: wasi.wasiImport,
});
wasi.initialize(instance as WebAssembly.Instance & { exports: R2Exports });
this.exports = instance.exports as R2Exports;
}
createCore(): R2Core {
const pointer = this.exports.r_core_new();
if (!pointer) throw new Error("radare2 failed to create an RCore");
return new R2Core(this, pointer);
}
command(core: number, command: string): string {
const input = this.putString(command);
const output = this.exports.r_core_cmd_str(core, input);
try {
return output ? this.getString(output) : "";
} finally {
if (output) this.exports.free(output);
this.exports.free(input);
}
}
freeCore(core: number): void {
this.exports.r_core_free(core);
}
assemble(architecture: Architecture, source: string): string {
this.stdout = "";
this.stderr = "";
const args = ["rasm2", "-a", architecture.arch, "-b", String(architecture.bits)];
if (architecture.cpu) args.push("-c", architecture.cpu);
args.push(source);
const strings = args.map((argument) => this.putString(argument));
const argv = this.exports.malloc(strings.length * 4);
const view = new DataView(this.exports.memory.buffer);
strings.forEach((pointer, index) => view.setUint32(argv + index * 4, pointer, true));
let result: number;
try {
result = this.exports.r_main_rasm2(strings.length, argv);
} finally {
strings.forEach((pointer) => this.exports.free(pointer));
this.exports.free(argv);
}
const hex = this.stdout.replace(/\s/g, "").toLowerCase();
if (result !== 0 || !hex || !/^[0-9a-f]+$/.test(hex) || hex.length % 2 !== 0) {
const detail = this.stderr.trim() || this.stdout.trim() || `rasm2 exited with ${result}`;
throw new Error(`Cannot assemble warrior: ${detail}`);
}
return hex;
}
private putString(value: string): number {
const bytes = encoder.encode(`${value}\0`);
const pointer = this.exports.malloc(bytes.length);
if (!pointer) throw new Error("radare2 WebAssembly ran out of memory");
new Uint8Array(this.exports.memory.buffer).set(bytes, pointer);
return pointer;
}
private getString(pointer: number): string {
const memory = new Uint8Array(this.exports.memory.buffer);
let end = pointer;
while (end < memory.length && memory[end] !== 0) end++;
return decoder.decode(memory.subarray(pointer, end));
}
}
export class R2Core {
private disposed = false;
constructor(private runtime: RadareRuntime, private pointer: number) {}
cmd(command: string): string {
if (this.disposed) throw new Error("radare2 core is already disposed");
return this.runtime.command(this.pointer, command);
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
this.runtime.freeCore(this.pointer);
}
}
+336
View File
@@ -0,0 +1,336 @@
import { architectureFromFilename, displayName } from "./architecture";
import { CombatEngine, type EngineSnapshot } from "./engine";
import { emptyMemory, emptyState, type AppState, type WarriorSource, type Workflow } from "./protocol";
import { RadareRuntime } from "./radare";
const MAX_CYCLES = 2000;
const MAX_HISTORY = 2048;
const TICKS_PER_BATCH = 8;
interface ActiveCombat {
pairing: [number, number];
round: number;
roundWins: [number, number];
cycles: number;
}
export class TournamentController {
private engine: CombatEngine;
private warriors: WarriorSource[] = [];
private pairings: Array<[number, number]> = [];
private scores: number[] = [];
private completedCombats = 0;
private pairingIndex = 0;
private active: ActiveCombat | null = null;
private autoRun = false;
private loopScheduled = false;
private workflow: Workflow = "idle";
private message = "Choose warrior files to begin.";
private consoleText = "[r2wars wasm]\nAll execution stays in this browser.";
private combatLog = "";
private fullLog = "";
private history: EngineSnapshot[] = [];
private historyIndex = -1;
private report: string | undefined;
constructor(runtime: RadareRuntime, private emit: (state: AppState) => void) {
this.engine = new CombatEngine(runtime);
}
load(warriors: WarriorSource[], emit = true): void {
this.stopLoop();
this.engine.dispose();
const normalized = warriors
.filter((warrior) => warrior.name.toLowerCase().endsWith(".asm"))
.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
const names = new Set<string>();
for (const warrior of normalized) {
architectureFromFilename(warrior.name);
const key = warrior.name.toLowerCase();
if (names.has(key)) throw new Error(`Duplicate warrior filename: ${warrior.name}`);
names.add(key);
if (!warrior.source.trim()) throw new Error(`${warrior.name}: source is empty`);
}
this.warriors = normalized;
this.pairings = [];
for (let first = 0; first < normalized.length; first++) {
for (let second = first + 1; second < normalized.length; second++) {
this.pairings.push([first, second]);
}
}
this.scores = normalized.map(() => 0);
this.completedCombats = 0;
this.pairingIndex = 0;
this.active = null;
this.history = [];
this.historyIndex = -1;
this.fullLog = "";
this.combatLog = "";
this.report = undefined;
const list = normalized.map((warrior) => ` • ${warrior.name}`).join("\n");
this.consoleText = `Tournament engine: radare2 6.2.0 WASI\nLoaded warriors (${normalized.length}):\n${list}`;
if (normalized.length < 2) {
this.workflow = "idle";
this.message = "At least two .asm warriors are required.";
} else {
this.workflow = "ready";
this.message = `${normalized.length} warriors loaded for ${this.pairings.length} battles. Start when ready.`;
}
if (emit) this.emitState();
}
start(warriors: WarriorSource[]): void {
this.load(warriors, false);
if (this.workflow === "ready") this.run();
else this.emitState();
}
command(command: string): void {
switch (command) {
case "cmd_state":
this.emitState();
break;
case "cmd_run":
this.run();
break;
case "cmd_stop":
this.pause();
break;
case "cmd_step":
case "cmd_next":
this.step();
break;
case "cmd_reset":
this.load(this.warriors);
break;
case "cmd_prevlog":
this.browseHistory(-1);
break;
case "cmd_nextlog":
this.browseHistory(1);
break;
}
}
fail(error: unknown): void {
this.stopLoop();
this.workflow = "error";
this.message = error instanceof Error ? error.message : String(error);
this.consoleText += `\n\nERROR\n${this.message}`;
this.emitState();
}
private run(): void {
if (this.workflow !== "ready" && this.workflow !== "paused") return;
this.autoRun = true;
this.workflow = "running";
this.message = "Tournament running. Pause at any time to inspect or step.";
this.engine.setStatus("Running");
this.emitState();
this.scheduleLoop();
}
private pause(): void {
if (this.workflow !== "running") return;
this.autoRun = false;
this.workflow = "paused";
this.message = "Paused. Resume continuous play or step exactly one cycle.";
this.engine.setStatus("Paused");
this.emitState();
}
private step(): void {
if (this.workflow !== "paused") return;
this.autoRun = false;
this.advanceOneTick();
if (this.completedCombats < this.pairings.length) {
this.workflow = "paused";
this.message = "Paused after one cycle. Step again or resume continuous play.";
this.engine.setStatus("Paused — advanced 1 cycle");
this.emitState();
}
}
private scheduleLoop(): void {
if (this.loopScheduled || !this.autoRun) return;
this.loopScheduled = true;
setTimeout(() => {
this.loopScheduled = false;
if (!this.autoRun) return;
try {
for (let tick = 0; tick < TICKS_PER_BATCH && this.autoRun; tick++) {
this.advanceOneTick(false);
if (this.workflow === "finished" || this.workflow === "error") break;
}
this.emitState();
this.scheduleLoop();
} catch (error) {
this.fail(error);
}
}, 16);
}
private advanceOneTick(emit = true): void {
if (!this.active) {
if (!this.prepareCombat()) return;
}
const result = this.engine.tick();
this.active!.cycles++;
this.recordHistory(result.snapshot);
if (result.deadPlayer !== null) {
const winner = 1 - result.deadPlayer;
const winnerGlobal = this.active!.pairing[winner];
const loserGlobal = this.active!.pairing[result.deadPlayer];
this.active!.roundWins[winner]++;
this.scores[winnerGlobal]++;
const roundLine = `Round ${this.active!.round + 1}: ${displayName(this.warriors[winnerGlobal].name)} wins (${this.active!.cycles} cycles)\n` +
` Defeated: ${displayName(this.warriors[loserGlobal].name)}\n` +
` Reason: ${result.deathReason}\n` +
` Instruction: ${result.deathInstruction}\n`;
this.combatLog += roundLine;
this.fullLog += roundLine;
if (this.active!.roundWins[winner] >= 2 || this.active!.round >= 2) {
this.finishCombat(winner);
} else {
this.active!.round++;
this.active!.cycles = 0;
this.engine.resetRound();
this.clearHistory(this.engine.snapshot());
}
} else if (this.active!.cycles >= MAX_CYCLES) {
const timeout = `Round ${this.active!.round + 1}: timeout (${this.active!.cycles} cycles)\n`;
this.combatLog += timeout;
this.fullLog += timeout;
if (this.active!.round >= 2) {
this.finishCombat(null);
} else {
this.active!.round++;
this.active!.cycles = 0;
this.engine.resetRound();
this.clearHistory(this.engine.snapshot());
}
}
if (emit) this.emitState();
}
private prepareCombat(): boolean {
if (this.pairingIndex >= this.pairings.length) {
this.finishTournament();
return false;
}
const pairing = this.pairings[this.pairingIndex];
const names = pairing.map((index) => displayName(this.warriors[index].name));
this.active = { pairing, round: 0, roundWins: [0, 0], cycles: 0 };
this.combatLog = `Battle ${this.pairingIndex + 1} / ${this.pairings.length}: ${names[0]} vs ${names[1]}\n`;
this.fullLog += this.combatLog;
const snapshot = this.engine.initialize([this.warriors[pairing[0]], this.warriors[pairing[1]]]);
this.clearHistory(snapshot);
return true;
}
private finishCombat(winner: number | null): void {
const names = this.engine.names();
const winnerName = winner === null ? "Draw" : names[winner];
const line = `Battle winner: ${winnerName}\n`;
this.combatLog += line;
this.fullLog += line;
this.completedCombats++;
this.pairingIndex++;
this.active = null;
if (this.completedCombats >= this.pairings.length) {
this.finishTournament();
} else if (!this.autoRun) {
this.workflow = "paused";
this.message = "Battle complete. Resume for the next battle.";
} else {
this.message = "Battle complete. Starting the next battle…";
}
}
private finishTournament(): void {
if (this.workflow === "finished") return;
this.stopLoop();
this.workflow = "finished";
this.message = "Tournament complete. Review the standings or download the report.";
const completed = new Date().toISOString();
this.fullLog += `Tournament finished ${completed}\n`;
this.report = `${this.buildScores()}\n\nFULL LOG\n========\n${this.fullLog}`;
this.consoleText = this.fullLog;
this.emitState();
}
private buildScores(): string {
if (this.warriors.length < 2) return "No scores yet.";
const standings = this.warriors
.map((warrior, index) => ({ name: displayName(warrior.name), points: this.scores[index] }))
.sort((left, right) => right.points - left.points || left.name.localeCompare(right.name));
const rows = standings.map((standing, index) => {
const name = standing.name.length > 28 ? `${standing.name.slice(0, 25)}...` : standing.name;
return `${String(index + 1).padStart(4)} ${name.padEnd(28)} ${String(standing.points).padStart(3)} round wins`;
});
return [
`STANDINGS — ${this.completedCombats} / ${this.pairings.length} battles complete`,
"",
"RANK WARRIOR RESULTS",
"---- ---------------------------- ----------------",
...rows,
].join("\n");
}
private clearHistory(snapshot: EngineSnapshot): void {
this.history = [snapshot];
this.historyIndex = 0;
}
private recordHistory(snapshot: EngineSnapshot): void {
this.history.push(snapshot);
if (this.history.length > MAX_HISTORY) this.history.shift();
this.historyIndex = this.history.length - 1;
}
private browseHistory(delta: number): void {
if (!this.history.length) return;
this.historyIndex = Math.max(0, Math.min(this.history.length - 1, this.historyIndex + delta));
this.emitState(this.history[this.historyIndex]);
}
private emitState(snapshot?: EngineSnapshot): void {
const state = emptyState();
const selected = this.historyIndex >= 0 ? this.history[this.historyIndex] : undefined;
const current = snapshot ?? selected ?? {
players: [state.player1, state.player2] as [typeof state.player1, typeof state.player2],
memory: emptyMemory(),
activePlayer: -1,
status: this.workflow === "loading" ? "Loading" : "Idle",
};
const browsing = this.historyIndex >= 0 && this.historyIndex < this.history.length - 1;
Object.assign(state, {
workflow: this.workflow,
message: this.message,
player1: current.players[0],
player2: current.players[1],
memory: current.memory,
console: this.active ? this.combatLog : this.consoleText,
status: browsing ? `History ${this.historyIndex + 1} / ${this.history.length} — ${current.status}` : current.status,
scores: this.buildScores(),
completedCombats: this.completedCombats,
totalCombats: this.pairings.length,
activePlayer: this.active ? current.activePlayer : -1,
historyPosition: this.historyIndex >= 0 ? this.historyIndex + 1 : 0,
historyCount: this.history.length,
canBrowseEarlier: this.historyIndex > 0,
canBrowseLater: this.historyIndex >= 0 && this.historyIndex < this.history.length - 1,
report: this.report,
} satisfies Partial<AppState>);
this.emit(state);
}
private stopLoop(): void {
this.autoRun = false;
}
}
+62
View File
@@ -0,0 +1,62 @@
/// <reference lib="webworker" />
import type { MainToWorker, WorkerToMain } from "./protocol";
import { architectureFromFilename } from "./architecture";
import { MAX_WARRIOR_SIZE } from "./engine";
import { RadareRuntime } from "./radare";
import { TournamentController } from "./tournament";
const context: DedicatedWorkerGlobalScope = self as unknown as DedicatedWorkerGlobalScope;
const runtime = new RadareRuntime();
let controller: TournamentController | null = null;
function post(message: WorkerToMain): void {
context.postMessage(message);
}
context.onmessage = async (event: MessageEvent<MainToWorker>) => {
const message = event.data;
try {
if (message.type === "init") {
await runtime.load(message.wasmUrl);
controller = new TournamentController(runtime, (state) => post({ type: "state", state }));
post({ type: "ready" });
return;
}
if (!controller) throw new Error("The WebAssembly engine is not ready yet");
if (message.type === "assemble") {
try {
const architecture = architectureFromFilename(message.warrior.name.trim());
const hex = runtime.assemble(architecture, message.warrior.source);
const bytes = hex.match(/.{2}/g)?.map((byte) => Number.parseInt(byte, 16)) ?? [];
const oversized = bytes.length > MAX_WARRIOR_SIZE;
post({
type: "assembly",
requestId: message.requestId,
ok: !oversized,
bytes,
size: bytes.length,
message: oversized
? `Compiles, but exceeds the ${MAX_WARRIOR_SIZE}-byte warrior limit.`
: "Compiles successfully.",
});
} catch (error) {
post({
type: "assembly",
requestId: message.requestId,
ok: false,
bytes: [],
size: 0,
message: error instanceof Error ? error.message : String(error),
});
}
return;
}
if (message.type === "load") controller.load(message.warriors);
else if (message.type === "start") controller.start(message.warriors);
else if (message.type === "command") controller.command(message.command);
} catch (error) {
if (controller) controller.fail(error);
else post({ type: "error", message: error instanceof Error ? error.message : String(error) });
}
};
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM", "WebWorker"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["vite/client", "node"]
},
"include": ["src", "vite*.config.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
import path from "node:path";
import { defineConfig } from "vite";
export default defineConfig({
base: "./",
root: import.meta.dirname,
publicDir: path.resolve(import.meta.dirname, "public"),
build: {
outDir: path.resolve(import.meta.dirname, "dist"),
emptyOutDir: true,
sourcemap: true,
},
worker: {
format: "es",
},
});
+35
View File
@@ -0,0 +1,35 @@
import path from "node:path";
import { defineConfig } from "vite";
export default defineConfig({
base: "/assets/",
root: import.meta.dirname,
publicDir: false,
define: {
"import.meta.env.VITE_R2WARS_ENGINE": JSON.stringify("dotnet"),
},
plugins: [{
name: "drop-unused-wasm-worker",
generateBundle(_options, bundle) {
for (const filename of Object.keys(bundle)) {
if (filename.startsWith("assets/worker-")) delete bundle[filename];
}
},
}],
build: {
outDir: path.resolve(import.meta.dirname, "../csharp/wwwroot/assets"),
emptyOutDir: true,
sourcemap: false,
rollupOptions: {
input: path.resolve(import.meta.dirname, "src/main.ts"),
output: {
entryFileNames: "r2wars-ui.js",
chunkFileNames: "[name]-[hash].js",
assetFileNames: "[name]-[hash][extname]",
},
},
},
worker: {
format: "es",
},
});
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
},
});