├── .gitignore ├── src ├── tsconfig.json ├── models │ ├── Cpu.ts │ ├── CpuFlags.ts │ └── CpuState.ts ├── css │ └── main.css ├── index.ts └── index.html ├── .dockerignore ├── .github ├── images │ └── space-invaders-architecture.png └── workflows │ ├── main.yml │ └── tags.yml ├── appsettings.Development.json ├── appsettings.json ├── Models ├── ShiftRegister.cs ├── Cpu.cs ├── CpuFlags.cs ├── CpuState.cs └── SpaceInvadersKey.cs ├── .config └── dotnet-tools.json ├── SpaceInvadersUI.csproj ├── OptionsModels └── ApiEndpoints.cs ├── Properties └── launchSettings.json ├── Services ├── ShiftRegisterService.cs ├── InputService.cs ├── BackgroundUpdateService.cs └── InterruptCheckService.cs ├── .vscode ├── tasks.json └── launch.json ├── Dockerfile ├── SignalR └── MainHub.cs ├── LICENSE.txt ├── Controllers ├── InterruptService.cs ├── InController.cs ├── OutController.cs ├── MemoryController.cs └── CpuController.cs ├── webpack.config.js ├── README.md ├── package.json ├── .devcontainer ├── Dockerfile ├── library-scripts │ └── azcli-debian.sh └── devcontainer.json ├── Program.cs ├── Startup.cs ├── .editorconfig └── docker-compose.yml /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | node_modules 4 | wwwroot/ -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" 4 | } 5 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | Dockerfile 4 | .config/ 5 | .devcontainer/ 6 | .vscode/ 7 | Properties/ 8 | node_modules/ 9 | wwwroot/ -------------------------------------------------------------------------------- /.github/images/space-invaders-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/21st-century-emulation/space-invaders-ui/HEAD/.github/images/space-invaders-architecture.png -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Models/ShiftRegister.cs: -------------------------------------------------------------------------------- 1 | namespace SpaceInvadersUI.Models 2 | { 3 | internal struct ShiftRegister 4 | { 5 | internal ushort Register; 6 | internal byte Offset; 7 | 8 | internal byte Value() => (byte)(Register >> (8 - Offset)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/models/Cpu.ts: -------------------------------------------------------------------------------- 1 | import CpuState from "./CpuState"; 2 | 3 | export default class Cpu { 4 | opcode: number; 5 | state: CpuState; 6 | id: string; 7 | 8 | constructor(id: string, state: CpuState, opcode: number) { 9 | this.id = id; 10 | this.state = state; 11 | this.opcode = opcode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Models/Cpu.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpaceInvadersUI.Models 4 | { 5 | public struct Cpu 6 | { 7 | 8 | [JsonInclude] 9 | public byte Opcode; 10 | 11 | [JsonInclude] 12 | public string Id; 13 | 14 | [JsonInclude] 15 | public CpuState State; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | #cycles-per-second-container { 2 | position:absolute; 3 | top: 0; 4 | left: 0; 5 | background: black; 6 | color: white; 7 | } 8 | 9 | .canvas-container { 10 | margin-right: auto; 11 | margin-left: auto; 12 | width: 448px; 13 | } 14 | 15 | .memory-table-container { 16 | overflow-y: auto; 17 | height: 100%; 18 | } -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-format": { 6 | "version": "5.0.211103", 7 | "commands": [ 8 | "dotnet-format" 9 | ] 10 | }, 11 | "dotnet-ef": { 12 | "version": "5.0.5", 13 | "commands": [ 14 | "dotnet-ef" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /SpaceInvadersUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /OptionsModels/ApiEndpoints.cs: -------------------------------------------------------------------------------- 1 | public class ApiEndpoints 2 | { 3 | public string ReadByteApi { get; init; } 4 | 5 | public string WriteByteApi { get; init; } 6 | 7 | public string ReadRangeApi { get; init; } 8 | 9 | public string InitialiseMemoryApi { get; init; } 10 | 11 | public string StartProgramApi { get; init; } 12 | 13 | public string GetStatusApi { get; init; } 14 | } -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SpaceInvadersUI": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/models/CpuFlags.ts: -------------------------------------------------------------------------------- 1 | export default class CpuFlags { 2 | sign: boolean; 3 | zero: boolean; 4 | auxCarry: boolean; 5 | parity: boolean; 6 | carry: boolean; 7 | 8 | constructor(sign: boolean, zero: boolean, auxCarry: boolean, parity: boolean, carry: boolean) { 9 | this.sign = sign; 10 | this.zero = zero; 11 | this.auxCarry = auxCarry; 12 | this.parity = parity; 13 | this.carry = carry; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Models/CpuFlags.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpaceInvadersUI.Models 4 | { 5 | public struct CpuFlags 6 | { 7 | [JsonInclude] 8 | public bool Sign; 9 | 10 | [JsonInclude] 11 | public bool Zero; 12 | 13 | [JsonInclude] 14 | public bool AuxCarry; 15 | 16 | [JsonInclude] 17 | public bool Parity; 18 | 19 | [JsonInclude] 20 | public bool Carry; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Services/ShiftRegisterService.cs: -------------------------------------------------------------------------------- 1 | using SpaceInvadersUI.Models; 2 | 3 | namespace SpaceInvadersUI.Services 4 | { 5 | public class ShiftRegisterService 6 | { 7 | private ShiftRegister _shiftRegister; 8 | 9 | internal byte GetValue() => _shiftRegister.Value(); 10 | 11 | internal void SetOffset(byte value) 12 | { 13 | _shiftRegister.Offset = (byte)(value & 0b111); ; 14 | } 15 | 16 | internal void SetRegister(byte value) 17 | { 18 | _shiftRegister.Register = (ushort)((_shiftRegister.Register >> 8) | (value << 8)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 2 | ARG GITHUB_USER 3 | ARG GITHUB_TOKEN 4 | WORKDIR /app 5 | 6 | RUN apt-get -qq update && apt-get -qqy --no-install-recommends install wget gnupg git unzip && \ 7 | curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ 8 | apt-get install -y nodejs 9 | 10 | COPY SpaceInvadersUI.csproj ./ 11 | RUN GITHUB_USER=${GITHUB_USER} GITHUB_TOKEN=${GITHUB_TOKEN} dotnet restore 12 | 13 | COPY package.json package-lock.json ./ 14 | RUN npm install -y 15 | 16 | COPY . . 17 | RUN npm run publish 18 | 19 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 20 | WORKDIR /app 21 | COPY --from=build /app/bin/Release/net5.0/publish/ ./ 22 | ENV ASPNETCORE_URLS=http://0.0.0.0:8080 23 | EXPOSE 8080 24 | ENTRYPOINT ["dotnet", "SpaceInvadersUI.dll"] -------------------------------------------------------------------------------- /Models/CpuState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpaceInvadersUI.Models 4 | { 5 | public struct CpuState 6 | { 7 | [JsonInclude] 8 | public CpuFlags Flags; 9 | 10 | [JsonInclude] 11 | public byte A; 12 | [JsonInclude] 13 | public byte B; 14 | [JsonInclude] 15 | public byte C; 16 | [JsonInclude] 17 | public byte D; 18 | [JsonInclude] 19 | public byte E; 20 | [JsonInclude] 21 | public byte H; 22 | [JsonInclude] 23 | public byte L; 24 | 25 | [JsonInclude] 26 | public ushort StackPointer; 27 | 28 | [JsonInclude] 29 | public ushort ProgramCounter; 30 | 31 | [JsonInclude] 32 | public ulong Cycles; 33 | 34 | [JsonInclude] 35 | public bool InterruptsEnabled; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/models/CpuState.ts: -------------------------------------------------------------------------------- 1 | import CpuFlags from "./CpuFlags"; 2 | 3 | export default class CpuState { 4 | a: number; 5 | b: number; 6 | c: number; 7 | d: number; 8 | e: number; 9 | h: number; 10 | l: number; 11 | stackPointer: number; 12 | programCounter: number; 13 | cycles: number; 14 | interruptsEnabled: boolean; 15 | flags: CpuFlags; 16 | 17 | constructor(a: number, b: number, c: number, d: number, e: number, h: number, l: number, stackPointer: number, programCounter: number, cycles: number, interruptsEnabled: boolean, flags: CpuFlags) { 18 | this.a = a; 19 | this.b = b; 20 | this.c = c; 21 | this.d = d; 22 | this.e = e; 23 | this.h = h; 24 | this.l = l; 25 | this.stackPointer = stackPointer; 26 | this.programCounter = programCounter; 27 | this.cycles = cycles; 28 | this.interruptsEnabled = interruptsEnabled; 29 | this.flags = flags; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SignalR/MainHub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.Extensions.Logging; 4 | using SpaceInvadersUI.Services; 5 | 6 | namespace SpaceInvadersUI.SignalR 7 | { 8 | public class MainHub : Hub 9 | { 10 | private readonly ILogger _log; 11 | private readonly InputService _inputService; 12 | 13 | public MainHub(ILogger log, InputService inputService) 14 | { 15 | _log = log ?? throw new ArgumentNullException(nameof(log)); 16 | _inputService = inputService ?? throw new ArgumentNullException(nameof(inputService)); 17 | } 18 | 19 | public void KeyUp(string key) 20 | { 21 | _log.LogInformation("Key up: {0}", key); 22 | _inputService.KeyUp(key); 23 | } 24 | 25 | public void KeyDown(string key) 26 | { 27 | _log.LogInformation("Key down: {0}", key); 28 | _inputService.KeyDown(key); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 David Tyler 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_and_deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | # Setting up Docker Builder 16 | - name: Set up Docker Builder 17 | uses: docker/setup-buildx-action@v1 18 | 19 | - name: Logging into GitHub Container Registry 20 | uses: docker/login-action@v1 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.repository_owner }} 24 | password: "${{ secrets.GITHUB_TOKEN }}" 25 | 26 | # Push to Github Container Registry 27 | - name: Pushing Image to Github Container Registry 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | file: Dockerfile 32 | build-args: | 33 | GITHUB_TOKEN=${{ secrets.PACKAGE_DOWNLOADER }} 34 | GITHUB_USER=${{ github.repository_owner }} 35 | push: true 36 | tags: ghcr.io/${{ github.repository }}:latest -------------------------------------------------------------------------------- /Controllers/InterruptService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using SpaceInvadersUI.Models; 5 | using SpaceInvadersUI.Services; 6 | 7 | namespace SpaceInvadersUI.Controllers 8 | { 9 | [Route("api/v1")] 10 | public class InterruptService : Controller 11 | { 12 | private readonly ILogger _logger; 13 | private readonly InterruptCheckService _interruptCheckService; 14 | 15 | public InterruptService(ILogger logger, InterruptCheckService interruptCheckService) 16 | { 17 | _logger = logger; 18 | _interruptCheckService = interruptCheckService; 19 | } 20 | 21 | [HttpPost, Route("checkInterrupts")] 22 | public async Task CheckInterrupts([FromBody] Cpu cpu) 23 | { 24 | _logger.LogDebug("Checking for interrupts on cpu {Id}", cpu.Id); 25 | var rstOperand = await _interruptCheckService.CheckForInterrupts(cpu); 26 | 27 | return Ok(rstOperand); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | module.exports = { 6 | entry: "./src/index.ts", 7 | output: { 8 | path: path.resolve(__dirname, "wwwroot"), 9 | filename: "[name].[chunkhash].js", 10 | publicPath: "/" 11 | }, 12 | resolve: { 13 | extensions: [".js", ".ts"] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.ts$/, 19 | use: "ts-loader" 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: [MiniCssExtractPlugin.loader, "css-loader"] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new CleanWebpackPlugin(), 29 | new HtmlWebpackPlugin({ 30 | template: "./src/index.html" 31 | }), 32 | new MiniCssExtractPlugin({ 33 | filename: "css/[name].[chunkhash].css" 34 | }) 35 | ] 36 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space Invaders UI 2 | 3 | This is the project containing the user facing portions of the 21st century 8080 emulator as placed into a Space Invaders console. 4 | 5 | ## How to run 6 | 7 | The application can be run from any computer with `docker` & `docker-compose` by running: 8 | 9 | ``` 10 | docker-compose up --build 11 | ``` 12 | 13 | The UI will be available on [http://localhost:8080] and the application can be started by POSTing the rom to [http://localhost:8080/api/v1/start]. e.g. 14 | 15 | ``` 16 | curl -X POST -F 'romForm=@~/invaders.rom' localhost:8080/api/v1/cpu/start 17 | ``` 18 | 19 | You will need to source the `invaders.rom` file yourself but it's quite easy to track down using bing. 20 | 21 | ## Architecture 22 | 23 | ![Space Invaders UI Architecture](.github/images/space-invaders-architecture.png?raw=true "Space Invaders UI - Architecture") 24 | 25 | ## Development 26 | 27 | ### Pre-requisites 28 | 29 | - Dotnet 5 & suitable text editor/IDE 30 | - To restore packages you require an access key to the github private package registry of the 21st-century organisation on github (stored in GITHUB_USER/GITHUB_PASSWORD environment variables) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space-invaders-ui", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "This is the project containing the user facing portions of the 21st century 8080 emulator as placed into a Space Invaders console.", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "webpack --mode=development --watch", 9 | "release": "webpack --mode=development", 10 | "publish": "npm run release && dotnet publish -c Release" 11 | }, 12 | "keywords": [ 13 | "8080", 14 | "Space Invaders" 15 | ], 16 | "author": { 17 | "email": "davet.code@gmail.com", 18 | "name": "David Tyler", 19 | "url": "https://blog.davetcode.com" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "clean-webpack-plugin": "4.0.0-alpha.0", 24 | "css-loader": "5.2.4", 25 | "html-webpack-plugin": "5.3.1", 26 | "mini-css-extract-plugin": "1.5.0", 27 | "ts-loader": "9.1.1", 28 | "typescript": "4.2.4", 29 | "webpack": "5.36.0", 30 | "webpack-cli": "4.6.0" 31 | }, 32 | "dependencies": { 33 | "@microsoft/signalr": "^5.0.5", 34 | "@types/node": "^15.0.0", 35 | "bootstrap": "^5.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/dotnet/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] .NET version: 5.0, 3.1, 2.1 4 | ARG VARIANT="5.0" 5 | FROM mcr.microsoft.com/vscode/devcontainers/dotnetcore:0-${VARIANT} 6 | 7 | # [Option] Install Node.js 8 | ARG INSTALL_NODE="true" 9 | ARG NODE_VERSION="lts/*" 10 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 11 | 12 | # [Option] Install Azure CLI 13 | ARG INSTALL_AZURE_CLI="false" 14 | COPY library-scripts/azcli-debian.sh /tmp/library-scripts/ 15 | RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ 16 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 17 | 18 | # [Optional] Uncomment this section to install additional OS packages. 19 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # && apt-get -y install --no-install-recommends 21 | 22 | # [Optional] Uncomment this line to install global node packages. 23 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: '*' 6 | 7 | jobs: 8 | build_and_deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Get the Ref 15 | id: get-ref 16 | uses: ankitvgupta/ref-to-tag-action@master 17 | with: 18 | ref: ${{ github.ref }} 19 | head_ref: ${{ github.head_ref }} 20 | 21 | # Setting up Docker Builder 22 | - name: Set up Docker Builder 23 | uses: docker/setup-buildx-action@v1 24 | 25 | - name: Logging into GitHub Container Registry 26 | uses: docker/login-action@v1 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.repository_owner }} 30 | password: "${{ secrets.GITHUB_TOKEN }}" 31 | 32 | # Push to Github Container Registry 33 | - name: Pushing Image to Github Container Registry 34 | uses: docker/build-push-action@v2 35 | with: 36 | context: . 37 | file: Dockerfile 38 | build-args: | 39 | GITHUB_TOKEN=${{ secrets.PACKAGE_DOWNLOADER }} 40 | GITHUB_USER=${{ github.repository_owner }} 41 | push: true 42 | tags: ghcr.io/${{ github.repository }}:${{ steps.get-ref.outputs.tag }} -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net5.0/FetchExecuteLoop.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Controllers/InController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using SpaceInvadersUI.Models; 4 | using SpaceInvadersUI.Services; 5 | 6 | namespace SpaceInvadersUI.Controllers 7 | { 8 | [Route("api/v1")] 9 | public class InController : Controller 10 | { 11 | private readonly ILogger _logger; 12 | private readonly ShiftRegisterService _shiftRegisterService; 13 | private readonly InputService _inputService; 14 | 15 | public InController(ILogger logger, ShiftRegisterService shiftRegisterService, InputService inputService) 16 | { 17 | _logger = logger; 18 | _shiftRegisterService = shiftRegisterService; 19 | _inputService = inputService; 20 | } 21 | 22 | [Route("in")] 23 | public Cpu In([FromQuery(Name = "operand1")] byte port, [FromBody] Cpu cpu) 24 | { 25 | _logger.LogInformation("Requesting IN for port {Port}", port); 26 | cpu.State.A = port switch 27 | { 28 | 0 => 0x0, // TODO INP0 29 | 1 => _inputService.GetPortStatus(0), 30 | 2 => _inputService.GetPortStatus(1), 31 | 3 => _shiftRegisterService.GetValue(), 32 | _ => 0x0 // TODO - Other ports 33 | }; 34 | return cpu; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using Serilog.Events; 6 | 7 | namespace SpaceInvadersUI 8 | { 9 | public class Program 10 | { 11 | public static int Main(string[] args) 12 | { 13 | var logConfig = new LoggerConfiguration() 14 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 15 | .MinimumLevel.Override("Serilog", LogEventLevel.Warning) 16 | .WriteTo.Console(); 17 | 18 | Log.Logger = logConfig.CreateLogger(); 19 | 20 | try 21 | { 22 | Log.Information("Starting web host"); 23 | CreateHostBuilder(args).Build().Run(); 24 | return 0; 25 | } 26 | catch (Exception ex) 27 | { 28 | Log.Fatal(ex, "Host terminated unexpectedly"); 29 | return 1; 30 | } 31 | finally 32 | { 33 | Log.CloseAndFlush(); 34 | } 35 | } 36 | 37 | public static IHostBuilder CreateHostBuilder(string[] args) => 38 | Host.CreateDefaultBuilder(args) 39 | .UseSerilog() 40 | .ConfigureWebHostDefaults(webBuilder => 41 | { 42 | webBuilder.UseStartup(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Controllers/OutController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using SpaceInvadersUI.Models; 4 | using SpaceInvadersUI.Services; 5 | 6 | namespace SpaceInvadersUI.Controllers 7 | { 8 | [Route("api/v1")] 9 | public class OutController : Controller 10 | { 11 | private readonly ILogger _logger; 12 | private readonly ShiftRegisterService _shiftRegisterService; 13 | 14 | public OutController(ILogger logger, ShiftRegisterService shiftRegisterService) 15 | { 16 | _logger = logger; 17 | _shiftRegisterService = shiftRegisterService; 18 | } 19 | 20 | [Route("out")] 21 | public Cpu Out([FromQuery(Name = "operand1")] byte port, [FromQuery(Name = "operand2")] byte value, [FromBody] Cpu cpu) 22 | { 23 | _logger.LogInformation("Requesting OUT for port {Port} = {Value}", port, value); 24 | switch (port) 25 | { 26 | case 2: 27 | _shiftRegisterService.SetOffset(value); 28 | break; 29 | case 3: 30 | // TODO SOUND1 port 31 | break; 32 | case 4: 33 | _shiftRegisterService.SetRegister(value); 34 | break; 35 | case 5: 36 | // TODO SOUND2 port 37 | break; 38 | case 6: 39 | // TODO WATCHDOG PORT 40 | break; 41 | } 42 | 43 | return cpu; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/azcli-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/master/script-library/docs/azcli.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./azcli-debian.sh 11 | 12 | set -e 13 | 14 | if [ "$(id -u)" -ne 0 ]; then 15 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 16 | exit 1 17 | fi 18 | 19 | export DEBIAN_FRONTEND=noninteractive 20 | 21 | # Install curl, apt-transport-https, lsb-release, or gpg if missing 22 | if ! dpkg -s apt-transport-https curl ca-certificates lsb-release > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then 23 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 24 | apt-get update 25 | fi 26 | apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates lsb-release gnupg2 27 | fi 28 | 29 | # Install the Azure CLI 30 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list 31 | curl -sL https://packages.microsoft.com/keys/microsoft.asc | (OUT=$(apt-key add - 2>&1) || echo $OUT) 32 | apt-get update 33 | apt-get install -y azure-cli 34 | echo "Done!" -------------------------------------------------------------------------------- /Models/SpaceInvadersKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpaceInvaders.Models 4 | { 5 | internal enum SpaceInvadersKey 6 | { 7 | Credit, 8 | P1Fire, 9 | P1Left, 10 | P1Right, 11 | P1Start, 12 | P2Fire, 13 | P2Left, 14 | P2Right, 15 | P2Start 16 | } 17 | 18 | internal static class SpaceInvadersKeyExtensions 19 | { 20 | internal static int PortIndex(this SpaceInvadersKey key) => key switch 21 | { 22 | SpaceInvadersKey.Credit => 0, 23 | SpaceInvadersKey.P1Fire => 0, 24 | SpaceInvadersKey.P1Left => 0, 25 | SpaceInvadersKey.P1Right => 0, 26 | SpaceInvadersKey.P1Start => 0, 27 | SpaceInvadersKey.P2Fire => 1, 28 | SpaceInvadersKey.P2Left => 1, 29 | SpaceInvadersKey.P2Right => 1, 30 | SpaceInvadersKey.P2Start => 0, 31 | _ => throw new ArgumentOutOfRangeException(nameof(key), key, null) 32 | }; 33 | 34 | internal static byte KeyDownMask(this SpaceInvadersKey key) => key switch 35 | { 36 | SpaceInvadersKey.Credit => 0b0000_0001, 37 | SpaceInvadersKey.P1Fire => 0b0001_0000, 38 | SpaceInvadersKey.P1Left => 0b0010_0000, 39 | SpaceInvadersKey.P1Right => 0b0100_0000, 40 | SpaceInvadersKey.P1Start => 0b0000_0100, 41 | SpaceInvadersKey.P2Fire => 0b0001_0000, 42 | SpaceInvadersKey.P2Left => 0b0010_0000, 43 | SpaceInvadersKey.P2Right => 0b0100_0000, 44 | SpaceInvadersKey.P2Start => 0b0000_0010, 45 | _ => throw new ArgumentOutOfRangeException(nameof(key), key, null) 46 | }; 47 | 48 | internal static byte KeyUpMask(this SpaceInvadersKey key) => (byte)~key.KeyDownMask(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Services/InputService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using SpaceInvaders.Models; 5 | 6 | namespace SpaceInvadersUI.Services 7 | { 8 | public class InputService 9 | { 10 | private readonly ILogger _logger; 11 | private readonly byte[] _portStatus = { 0b0000_1000, 0b0000_0000 }; 12 | 13 | private readonly Dictionary _keyMap = new() 14 | { 15 | { "ArrowRight", SpaceInvadersKey.P1Right }, 16 | { "ArrowLeft", SpaceInvadersKey.P1Left }, 17 | { "ArrowUp", SpaceInvadersKey.P1Fire }, 18 | { "a", SpaceInvadersKey.P2Left }, 19 | { "d", SpaceInvadersKey.P2Right }, 20 | { "w", SpaceInvadersKey.P2Fire }, 21 | { "1", SpaceInvadersKey.P1Start }, 22 | { "2", SpaceInvadersKey.P2Start }, 23 | { "Enter", SpaceInvadersKey.Credit } 24 | }; 25 | 26 | public InputService(ILogger logger) 27 | { 28 | _logger = logger; 29 | } 30 | 31 | internal byte GetPortStatus(int port) 32 | { 33 | if (port < 0 || port > 1) throw new ArgumentException(nameof(port)); 34 | 35 | return _portStatus[port]; 36 | } 37 | 38 | internal void KeyUp(string key) 39 | { 40 | if (_keyMap.ContainsKey(key)) 41 | { 42 | _logger.LogInformation("Registered key up event for key {Key}", key); 43 | _portStatus[_keyMap[key].PortIndex()] &= _keyMap[key].KeyUpMask(); 44 | } 45 | } 46 | 47 | internal void KeyDown(string key) 48 | { 49 | if (_keyMap.ContainsKey(key)) 50 | { 51 | _logger.LogInformation("Registered key down event for key {Key}", key); 52 | _portStatus[_keyMap[key].PortIndex()] |= _keyMap[key].KeyDownMask(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using SpaceInvadersUI.Services; 8 | using SpaceInvadersUI.SignalR; 9 | 10 | namespace SpaceInvadersUI 11 | { 12 | public class Startup 13 | { 14 | private readonly IConfiguration _configuration; 15 | 16 | public Startup(IHostEnvironment env) 17 | { 18 | _configuration = new ConfigurationBuilder() 19 | .SetBasePath(env.ContentRootPath) 20 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 21 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 22 | .AddEnvironmentVariables() 23 | .Build(); 24 | } 25 | 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddHealthChecks(); 29 | services.AddControllers(); 30 | services.AddSingleton(); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddHostedService(); 34 | services.AddHttpClient(); 35 | services.AddSignalR().AddJsonProtocol((options) => 36 | { 37 | options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 38 | options.PayloadSerializerOptions.IncludeFields = true; 39 | }); 40 | services.Configure(_configuration.GetSection("ApiEndpoints")); 41 | } 42 | 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | if (env.IsDevelopment()) 46 | { 47 | app.UseDeveloperExceptionPage(); 48 | } 49 | 50 | app.UseRouting(); 51 | app.UseDefaultFiles(); 52 | app.UseStaticFiles(); 53 | app.UseHealthChecks("/status"); 54 | //app.UseSerilogRequestLogging(); 55 | 56 | app.UseEndpoints(endpoints => 57 | { 58 | endpoints.MapHealthChecks("/status"); 59 | endpoints.MapControllers(); 60 | endpoints.MapHub("/signalr/hub"); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update 'VARIANT' to pick a .NET Core version: 2.1, 3.1, 5.0 9 | "VARIANT": "5.0", 10 | // Options 11 | "INSTALL_NODE": "true", 12 | "NODE_VERSION": "lts/*", 13 | "INSTALL_AZURE_CLI": "false" 14 | } 15 | }, 16 | 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "terminal.integrated.shell.linux": "/bin/bash" 20 | }, 21 | 22 | // Add the IDs of extensions you want installed when the container is created. 23 | "extensions": [ 24 | "ms-dotnettools.csharp" 25 | ], 26 | 27 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 28 | // "forwardPorts": [5000, 5001], 29 | 30 | // [Optional] To reuse of your local HTTPS dev cert: 31 | // 32 | // 1. Export it locally using this command: 33 | // * Windows PowerShell: 34 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 35 | // * macOS/Linux terminal: 36 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 37 | // 38 | // 2. Uncomment these 'remoteEnv' lines: 39 | // "remoteEnv": { 40 | // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", 41 | // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", 42 | // }, 43 | // 44 | // 3. Do one of the following depending on your scenario: 45 | // * When using GitHub Codespaces and/or Remote - Containers: 46 | // 1. Start the container 47 | // 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer 48 | // 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https" 49 | // 50 | // * If only using Remote - Containers with a local container, uncomment this line instead: 51 | // "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ], 52 | 53 | // Use 'postCreateCommand' to run commands after the container is created. 54 | // "postCreateCommand": "dotnet restore", 55 | 56 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 57 | "remoteUser": "vscode" 58 | } 59 | -------------------------------------------------------------------------------- /Controllers/MemoryController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Options; 9 | 10 | namespace SpaceInvadersUI.Controllers 11 | { 12 | [Route("api/v1/memory")] 13 | public class MemoryController : ControllerBase 14 | { 15 | private readonly ILogger _log; 16 | private readonly IHttpClientFactory _clientFactory; 17 | private readonly ApiEndpoints _memoryBusOptions; 18 | 19 | public MemoryController(ILogger log, IHttpClientFactory clientFactory, IOptions memoryBusOptions) 20 | { 21 | _log = log; 22 | _clientFactory = clientFactory; 23 | _memoryBusOptions = memoryBusOptions.Value; 24 | } 25 | 26 | [Route("dump"), HttpGet] 27 | [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] 28 | public async Task DumpMemory(string id) 29 | { 30 | if (id == null) return BadRequest("Missing id for computer"); 31 | 32 | _log.LogInformation("Dumping all memory"); 33 | using (var client = _clientFactory.CreateClient()) 34 | { 35 | var response = await client.GetAsync($"{_memoryBusOptions.ReadRangeApi}?id={id}&address={0}&length={0x10000}"); 36 | response.EnsureSuccessStatusCode(); 37 | var base64Range = await response.Content.ReadAsStringAsync(); 38 | var dbDump = Convert.FromBase64String(base64Range); 39 | return Ok(dbDump); 40 | } 41 | } 42 | 43 | [Route("inspect"), HttpGet] 44 | [ProducesResponseType(typeof(byte), StatusCodes.Status200OK)] 45 | public async Task InspectValue(string id, ushort address) 46 | { 47 | if (id == null) return BadRequest("Missing id for computer"); 48 | if (id == null) return BadRequest("Missing address to inspect"); 49 | 50 | _log.LogInformation("Inspecting value at address {0:X4}", address); 51 | using (var client = _clientFactory.CreateClient()) 52 | { 53 | var response = await client.GetAsync($"{_memoryBusOptions.ReadByteApi}?id={id}&address={address}"); 54 | response.EnsureSuccessStatusCode(); 55 | return Ok(await response.Content.ReadAsStringAsync()); 56 | } 57 | } 58 | 59 | [Route("reset"), HttpPost] 60 | public async Task InjectValue(string id, ushort address, byte value) 61 | { 62 | if (id == null) return BadRequest("Missing id for computer"); 63 | if (id == null) return BadRequest("Missing address to inspect"); 64 | if (id == null) return BadRequest("Missing value to set"); 65 | 66 | _log.LogInformation("Injecting value {0:X2} at address {1:X4} for computer {2}", value, address, id); 67 | using (var client = _clientFactory.CreateClient()) 68 | { 69 | var response = await client.PostAsync($"{_memoryBusOptions.WriteByteApi}?id={id}&address={address}&value={value}", new StringContent("")); 70 | response.EnsureSuccessStatusCode(); 71 | return Ok(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Services/BackgroundUpdateService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Text.Json; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.SignalR; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using SpaceInvadersUI.Models; 12 | using SpaceInvadersUI.SignalR; 13 | 14 | namespace SpaceInvadersUI.Services 15 | { 16 | internal class BackgroundUpdateService : IHostedService, IDisposable 17 | { 18 | private readonly ILogger _logger; 19 | private Timer _timer; 20 | private readonly IServiceScopeFactory _serviceScopeFactory; 21 | private readonly ApiEndpoints _apiEndpoints; 22 | 23 | public BackgroundUpdateService(ILogger logger, IServiceScopeFactory serviceScopeFactory, IOptions apiEndpoints) 24 | { 25 | _logger = logger; 26 | _serviceScopeFactory = serviceScopeFactory; 27 | _apiEndpoints = apiEndpoints.Value; 28 | } 29 | 30 | public Task StartAsync(CancellationToken cancellationToken) 31 | { 32 | _logger.LogInformation("Background Update Service is starting."); 33 | 34 | _timer = new Timer(UpdateCpuOnClients, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); 35 | 36 | return Task.CompletedTask; 37 | } 38 | 39 | private async void UpdateCpuOnClients(object state) 40 | { 41 | using (var scope = _serviceScopeFactory.CreateScope()) 42 | { 43 | var clientFactory = scope.ServiceProvider.GetRequiredService(); 44 | var hubContext = scope.ServiceProvider.GetRequiredService>(); 45 | using (var client = clientFactory.CreateClient()) 46 | { 47 | var response = await client.GetAsync($"{_apiEndpoints.GetStatusApi}"); 48 | response.EnsureSuccessStatusCode(); 49 | var data = await response.Content.ReadAsStringAsync(); 50 | var cpu = JsonSerializer.Deserialize(data, new JsonSerializerOptions 51 | { 52 | IncludeFields = true, 53 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 54 | }); 55 | 56 | if (cpu.Id != null) 57 | { 58 | // Get all 0x100 bytes used for RAM to display in the UI 59 | var memoryResponse = await client.GetAsync($"{_apiEndpoints.ReadRangeApi}?id={cpu.Id}&address={0x2000}&length={0x100}"); 60 | memoryResponse.EnsureSuccessStatusCode(); 61 | var memoryBase64String = await memoryResponse.Content.ReadAsStringAsync(); 62 | 63 | await hubContext.Clients.All.SendAsync("cpuUpdated", cpu, memoryBase64String); 64 | } 65 | } 66 | } 67 | } 68 | 69 | public Task StopAsync(CancellationToken cancellationToken) 70 | { 71 | _logger.LogInformation("Background Update Service is stopping."); 72 | 73 | _timer?.Change(Timeout.Infinite, 0); 74 | 75 | return Task.CompletedTask; 76 | } 77 | 78 | public void Dispose() 79 | { 80 | _timer?.Dispose(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Services/InterruptCheckService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.SignalR; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Options; 9 | using SpaceInvadersUI.Models; 10 | using SpaceInvadersUI.SignalR; 11 | 12 | namespace SpaceInvadersUI.Services 13 | { 14 | public class InterruptCheckService 15 | { 16 | private const int CyclesPerFrame = 34132; 17 | private const int CyclesPerHalfFrame = 17066; 18 | private readonly ILogger _logger; 19 | private ulong _lastSeenCycles = 0; 20 | private int _countDownToVBlank = 0; // TODO - Triggers vblank immediately to see what's going on, set back to something longer 21 | private int _countDownToHalfScreen = CyclesPerHalfFrame; 22 | private readonly IServiceScopeFactory _serviceScopeFactory; 23 | private readonly ApiEndpoints _memoryBusOptions; 24 | 25 | 26 | public InterruptCheckService(ILogger logger, IServiceScopeFactory serviceScopeFactory, IOptions memoryBusOptions) 27 | { 28 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 29 | _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); 30 | _memoryBusOptions = memoryBusOptions?.Value ?? throw new ArgumentNullException(nameof(memoryBusOptions)); 31 | } 32 | 33 | internal async Task CheckForInterrupts(Cpu cpu) 34 | { 35 | if (cpu.State.Cycles < _lastSeenCycles) throw new ArgumentException("Invalid call to check interrupts, cycles is less than last time it was called", nameof(cpu)); 36 | var cycleDiff = (int)(cpu.State.Cycles - _lastSeenCycles); 37 | _lastSeenCycles = cpu.State.Cycles; 38 | 39 | _countDownToVBlank -= cycleDiff; 40 | _countDownToHalfScreen -= cycleDiff; 41 | 42 | // Check for half screen interrupts first - this generates RST 2 which is a CALL 0x0008 43 | if (_countDownToHalfScreen < 0) 44 | { 45 | _countDownToHalfScreen += CyclesPerFrame; 46 | _logger.LogInformation("Firing half screen interrupt"); 47 | return 2; // Don't need to check for both interrupts 48 | } 49 | 50 | // Then check for vblank interrupts - this generates RST 4 which is a CALL 0x0010 51 | if (_countDownToVBlank < 0) 52 | { 53 | _countDownToVBlank += CyclesPerFrame; 54 | _logger.LogInformation("Firing vblank interrupt and calling external service"); 55 | 56 | using (var scope = _serviceScopeFactory.CreateScope()) 57 | { 58 | var httpClientFactory = scope.ServiceProvider.GetRequiredService(); 59 | var hubContext = scope.ServiceProvider.GetRequiredService>(); 60 | using (var client = httpClientFactory.CreateClient()) 61 | { 62 | var vramRangeResponse = await client.GetAsync($"{_memoryBusOptions.ReadRangeApi}?id={cpu.Id}&address={0x2400}&length={0x1C00}"); 63 | vramRangeResponse.EnsureSuccessStatusCode(); 64 | var base64String = await vramRangeResponse.Content.ReadAsStringAsync(); 65 | await hubContext.Clients.All.SendAsync("vblank", Convert.FromBase64String(base64String)); 66 | } 67 | } 68 | return 1; 69 | } 70 | 71 | return null; 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Controllers/CpuController.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Options; 14 | using SpaceInvadersUI.Models; 15 | 16 | namespace SpaceInvadersUI.Controllers 17 | { 18 | [Route("api/v1/cpu")] 19 | public class ExecuteController : ControllerBase 20 | { 21 | private readonly ILogger _log; 22 | private readonly IHttpClientFactory _clientFactory; 23 | private readonly ApiEndpoints _apiEndpoints; 24 | 25 | public ExecuteController(ILogger log, IHttpClientFactory clientFactory, IOptions apiEndpoints) 26 | { 27 | _log = log; 28 | _clientFactory = clientFactory; 29 | _apiEndpoints = apiEndpoints.Value; 30 | } 31 | 32 | [Route("initialise"), HttpPost] 33 | [ProducesResponseType(typeof(Cpu), StatusCodes.Status200OK)] 34 | public IActionResult InitializeCpu([FromBody] Cpu cpu) 35 | { 36 | // Space invaders doesn't need to do any pre-init, even SP is set from code 37 | return Ok(cpu); 38 | } 39 | 40 | [Route("start"), HttpPost] 41 | [ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)] 42 | public async Task StartProgram(IFormFile romForm) 43 | { 44 | if (romForm == null) return BadRequest("Missing rom file on start request"); 45 | _log.LogInformation("Requesting CPU start with new ROM"); 46 | 47 | var romBytes = new byte[0x10000]; 48 | using var ms = new MemoryStream(); 49 | await romForm.CopyToAsync(ms); 50 | var sentBytes = ms.ToArray(); 51 | Array.Copy(sentBytes, romBytes, Math.Min(0x10000, sentBytes.LongLength)); 52 | var computerId = Guid.NewGuid(); 53 | 54 | using (var client = _clientFactory.CreateClient()) 55 | { 56 | var rom = new 57 | { 58 | Id = computerId.ToString(), 59 | ProgramState = Convert.ToBase64String(romBytes), 60 | }; 61 | var initResponse = await client.PostAsync( 62 | $"{_apiEndpoints.InitialiseMemoryApi}", 63 | new StringContent(JsonSerializer.Serialize(rom, new JsonSerializerOptions 64 | { 65 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 66 | IncludeFields = true, 67 | })) 68 | { 69 | Headers = { ContentType = new MediaTypeHeaderValue("application/json") } 70 | }); 71 | 72 | initResponse.EnsureSuccessStatusCode(); 73 | 74 | var startResponse = await client.PostAsync($"{_apiEndpoints.StartProgramApi}?id={computerId}", new StringContent("")); 75 | startResponse.EnsureSuccessStatusCode(); 76 | } 77 | 78 | _log.LogInformation("ROM loaded into shared CPU queue"); 79 | return Ok(computerId); 80 | } 81 | 82 | [Route("status"), HttpGet] 83 | public IActionResult CheckProgramStatus(string id) 84 | { 85 | if (id == null) return BadRequest("Missing id for computer"); 86 | _log.LogInformation("Requesting CPU status"); 87 | 88 | throw new NotImplementedException("TODO - Implement status function"); 89 | } 90 | 91 | [Route("reset"), HttpPost] 92 | public IActionResult StopProgram(string id) 93 | { 94 | _log.LogInformation("Requesting CPU stop and reset"); 95 | 96 | throw new NotImplementedException("TODO - Implement stop function"); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.css" 2 | import "./css/main.css"; 3 | import * as signalR from "@microsoft/signalr"; 4 | import Cpu from "./models/Cpu"; 5 | 6 | const htmlElement: HTMLHtmlElement = document.querySelector("html"); 7 | const aElement: HTMLElement = document.querySelector("#a-data"); 8 | const bElement: HTMLElement = document.querySelector("#b-data"); 9 | const cElement: HTMLElement = document.querySelector("#c-data"); 10 | const dElement: HTMLElement = document.querySelector("#d-data"); 11 | const eElement: HTMLElement = document.querySelector("#e-data"); 12 | const hElement: HTMLElement = document.querySelector("#h-data"); 13 | const lElement: HTMLElement = document.querySelector("#l-data"); 14 | const signElement: HTMLElement = document.querySelector("#sign-data"); 15 | const zeroElement: HTMLElement = document.querySelector("#zero-data"); 16 | const auxCarryElement: HTMLElement = document.querySelector("#auxcarry-data"); 17 | const parityElement: HTMLElement = document.querySelector("#parity-data"); 18 | const carryElement: HTMLElement = document.querySelector("#carry-data"); 19 | const stackPointerElement: HTMLElement = document.querySelector("#stack-pointer-data"); 20 | const programCounterElement: HTMLElement = document.querySelector("#program-counter-data"); 21 | const interruptsEnabledElement: HTMLElement = document.querySelector("#interrupts-enabled-data"); 22 | const cyclesPerSecondElement: HTMLElement = document.querySelector("#cycles-per-second"); 23 | const uiCanvas: HTMLCanvasElement = document.querySelector("#ui-canvas"); 24 | const backingCanvas: HTMLCanvasElement = document.querySelector("#backing-canvas"); 25 | const canvasContext = uiCanvas.getContext("2d"); 26 | const backingCanvasContext = backingCanvas.getContext("2d"); 27 | 28 | let memoryElements: HTMLElement[] = []; 29 | for (let ii = 0; ii < 0x100; ii++) { 30 | memoryElements.push(document.querySelector(`#memory-value-${(0x2000 + ii).toString(16).toUpperCase()}`)); 31 | } 32 | 33 | const connection = new signalR.HubConnectionBuilder() 34 | .withUrl("/signalr/hub") 35 | .withAutomaticReconnect() 36 | .build(); 37 | 38 | let timeSinceLastUpdate: Date = null; 39 | let cyclesAtLastUpdate: number = 0; 40 | connection.on("cpuUpdated", (cpu: Cpu, memoryBase64String: string) => { 41 | aElement.textContent = `0x${cpu.state.a.toString(16).toUpperCase()}`; 42 | bElement.textContent = `0x${cpu.state.b.toString(16).toUpperCase()}`; 43 | cElement.textContent = `0x${cpu.state.c.toString(16).toUpperCase()}`; 44 | dElement.textContent = `0x${cpu.state.d.toString(16).toUpperCase()}`; 45 | eElement.textContent = `0x${cpu.state.e.toString(16).toUpperCase()}`; 46 | hElement.textContent = `0x${cpu.state.h.toString(16).toUpperCase()}`; 47 | lElement.textContent = `0x${cpu.state.l.toString(16).toUpperCase()}`; 48 | signElement.textContent = `${cpu.state.flags.sign ? "True" : "False"}`; 49 | zeroElement.textContent = `${cpu.state.flags.zero ? "True" : "False"}`; 50 | auxCarryElement.textContent = `${cpu.state.flags.auxCarry ? "True" : "False"}`; 51 | parityElement.textContent = `${cpu.state.flags.parity ? "True" : "False"}`; 52 | carryElement.textContent = `${cpu.state.flags.carry ? "True" : "False"}`; 53 | stackPointerElement.textContent = `0x${cpu.state.stackPointer.toString(16).toUpperCase()}`; 54 | programCounterElement.textContent = `0x${cpu.state.programCounter.toString(16).toUpperCase()}`; 55 | interruptsEnabledElement.textContent = `${cpu.state.interruptsEnabled ? "True" : "False"}`; 56 | 57 | const memoryArray = atob(memoryBase64String); 58 | for (let ii = 0; ii < memoryArray.length; ii++) { 59 | memoryElements[ii].textContent = `0x${memoryArray.charCodeAt(ii).toString(16).toUpperCase()}`; 60 | } 61 | 62 | const updateTime = new Date(); 63 | if (timeSinceLastUpdate !== null) { 64 | const diffMs = updateTime.getTime() - timeSinceLastUpdate.getTime(); 65 | const diffCycles = cpu.state.cycles - cyclesAtLastUpdate; 66 | 67 | // If cycles haven't changed don't change the performance counter 68 | if (diffCycles !== 0) { 69 | const cyclesPerSecond = (diffCycles * 1000) / diffMs; 70 | cyclesPerSecondElement.textContent = `${cyclesPerSecond.toFixed(2)}Hz`; 71 | timeSinceLastUpdate = updateTime; 72 | cyclesAtLastUpdate = cpu.state.cycles; 73 | } 74 | } else { 75 | timeSinceLastUpdate = updateTime; 76 | cyclesAtLastUpdate = cpu.state.cycles; 77 | } 78 | }); 79 | 80 | connection.on("vblank", (vram: string) => { 81 | const vramBytes = atob(vram); 82 | const arr = new Uint8ClampedArray(224 * 256 * 4); 83 | 84 | for (let arrIx = 0; arrIx < arr.length; arrIx += 4) { 85 | const x = (arrIx / 4) % 224; 86 | const y = Math.floor((arrIx / 4) / 256); 87 | const rX = 255 - y; 88 | const rY = x; 89 | const vramIx = Math.floor((rY * 256 + rX) / 8); 90 | const pixel = rX % 8; 91 | const value = ((vramBytes.charCodeAt(vramIx) >> pixel) & 1) == 1 ? 0xFF : 0x0; 92 | 93 | arr[arrIx] = value; // R 94 | arr[arrIx + 1] = value; // G 95 | arr[arrIx + 2] = value; // B 96 | arr[arrIx + 3] = 0xFF; // A 97 | } 98 | 99 | const imageData = new ImageData(arr, 224); 100 | backingCanvasContext.putImageData(imageData, 0, 0); 101 | canvasContext.drawImage(backingCanvas, 0, 0, 224, 256, 0, 0, 448, 512); 102 | }); 103 | 104 | connection.start().catch(err => document.write(err)); 105 | 106 | htmlElement.addEventListener("keyup", (e: KeyboardEvent) => { 107 | connection.send("keyUp", e.key); 108 | }); 109 | 110 | htmlElement.addEventListener("keydown", (e: KeyboardEvent) => { 111 | connection.send("keyDown", e.key); 112 | }); -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All files 4 | [*] 5 | indent_style = space 6 | # HTML 7 | indent_size = 2 8 | # Code files 9 | [*.{cs,csx,vb,vbx}] 10 | indent_size = 4 11 | insert_final_newline = true 12 | charset = utf-8-bom 13 | ############################### 14 | # .NET Coding Conventions # 15 | ############################### 16 | [*.{cs,vb}] 17 | # Organize usings 18 | dotnet_sort_system_directives_first = true 19 | dotnet_separate_import_directive_groups = false 20 | # this. preferences 21 | dotnet_style_qualification_for_field = false:error 22 | dotnet_style_qualification_for_property = false:error 23 | dotnet_style_qualification_for_method = false:error 24 | dotnet_style_qualification_for_event = false:error 25 | # Language keywords vs BCL types preferences 26 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 27 | dotnet_style_predefined_type_for_member_access = true:error 28 | # Parentheses preferences 29 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 30 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 31 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 32 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 33 | # Modifier preferences 34 | dotnet_style_require_accessibility_modifiers = always:error 35 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 36 | dotnet_style_readonly_field = true:error 37 | # Expression-level preferences 38 | dotnet_style_object_initializer = true:suggestion 39 | dotnet_style_collection_initializer = true:suggestion 40 | dotnet_style_explicit_tuple_names = true:suggestion 41 | dotnet_style_null_propagation = true:suggestion 42 | dotnet_style_coalesce_expression = true:suggestion 43 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 44 | dotnet_prefer_inferred_tuple_names = true:suggestion 45 | dotnet_prefer_inferred_anonymous_type_member_names = true:error 46 | dotnet_style_prefer_auto_properties = true:error 47 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 48 | dotnet_style_prefer_conditional_expression_over_return = true:silent 49 | ############################### 50 | # Naming Conventions # 51 | ############################### 52 | # Style Definitions 53 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 54 | # Use PascalCase for constant fields 55 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error 56 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 57 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 58 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 59 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 60 | dotnet_naming_symbols.constant_fields.required_modifiers = const 61 | # Use PascalCase for public fields 62 | dotnet_naming_rule.pascal_case_for_public_fields.severity = error 63 | dotnet_naming_rule.pascal_case_for_public_fields.symbols = public_fields 64 | dotnet_naming_rule.pascal_case_for_public_fields.style = pascal_case_style 65 | dotnet_naming_symbols.public_fields.applicable_kinds = field 66 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public 67 | # Interfaces must be PascalCase and have an I prefix 68 | dotnet_naming_rule.interfaces_start_with_I.severity = error 69 | dotnet_naming_rule.interfaces_start_with_I.symbols = any_interface 70 | dotnet_naming_rule.interfaces_start_with_I.style = I_prefix_style 71 | dotnet_naming_symbols.any_interface.applicable_accessibilities = * 72 | dotnet_naming_symbols.any_interface.applicable_kinds = interface 73 | # Classes, structs, methods, enums, events, properties, namespaces, delegates must be PascalCase 74 | dotnet_naming_rule.general_naming.severity = error 75 | dotnet_naming_rule.general_naming.symbols = general 76 | dotnet_naming_rule.general_naming.style = pascal_case_style 77 | dotnet_naming_symbols.general.applicable_kinds = class,struct,enum,property,method,event,namespace,delegate 78 | dotnet_naming_symbols.general.applicable_accessibilities = * 79 | # Everything else is camelCase 80 | dotnet_naming_rule.everything_else_naming.severity = error 81 | dotnet_naming_rule.everything_else_naming.symbols = everything_else 82 | dotnet_naming_rule.everything_else_naming.style = camel_case_style 83 | dotnet_naming_symbols.everything_else.applicable_kinds = * 84 | dotnet_naming_symbols.everything_else.applicable_accessibilities = * 85 | 86 | ############################### 87 | # C# Coding Conventions # 88 | ############################### 89 | [*.cs] 90 | # var preferences 91 | csharp_style_var_for_built_in_types = true:error 92 | csharp_style_var_when_type_is_apparent = true:error 93 | csharp_style_var_elsewhere = true:error 94 | # Expression-bodied members 95 | csharp_style_expression_bodied_methods = false:silent 96 | csharp_style_expression_bodied_constructors = false:silent 97 | csharp_style_expression_bodied_operators = false:silent 98 | csharp_style_expression_bodied_properties = true:silent 99 | csharp_style_expression_bodied_indexers = true:silent 100 | csharp_style_expression_bodied_accessors = true:silent 101 | # Pattern matching preferences 102 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 103 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 104 | # Null-checking preferences 105 | csharp_style_throw_expression = true:suggestion 106 | csharp_style_conditional_delegate_call = true:suggestion 107 | # Modifier preferences 108 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 109 | # Expression-level preferences 110 | csharp_prefer_braces = true:silent 111 | csharp_style_deconstructed_variable_declaration = true:suggestion 112 | csharp_prefer_simple_default_expression = true:suggestion 113 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 114 | csharp_style_inlined_variable_declaration = true:suggestion 115 | ############################### 116 | # C# Formatting Rules # 117 | ############################### 118 | # New line preferences 119 | csharp_new_line_before_open_brace = all 120 | csharp_new_line_before_else = true 121 | csharp_new_line_before_catch = true 122 | csharp_new_line_before_finally = true 123 | csharp_new_line_before_members_in_object_initializers = true 124 | csharp_new_line_before_members_in_anonymous_types = true 125 | csharp_new_line_between_query_expression_clauses = true 126 | # Indentation preferences 127 | csharp_indent_case_contents = true 128 | csharp_indent_switch_labels = true 129 | csharp_indent_labels = flush_left 130 | # Space preferences 131 | csharp_space_after_cast = false 132 | csharp_space_after_keywords_in_control_flow_statements = true 133 | csharp_space_between_method_call_parameter_list_parentheses = false 134 | csharp_space_between_method_declaration_parameter_list_parentheses = false 135 | csharp_space_between_parentheses = false 136 | csharp_space_before_colon_in_inheritance_clause = true 137 | csharp_space_after_colon_in_inheritance_clause = true 138 | csharp_space_around_binary_operators = before_and_after 139 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 140 | csharp_space_between_method_call_name_and_opening_parenthesis = false 141 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 142 | # Wrapping preferences 143 | csharp_preserve_single_line_statements = true 144 | csharp_preserve_single_line_blocks = true -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | space-invaders-ui-dev: 5 | 6 | services: 7 | memory-bus: 8 | image: ghcr.io/21st-century-emulation/memory-bus-8080:latest 9 | ports: 10 | - "8000:8080" 11 | restart: always 12 | networks: 13 | - space-invaders-ui-dev 14 | 15 | spaceinvadersui: 16 | image: space-invaders-ui:latest 17 | depends_on: 18 | - "emulator-backend-8080" 19 | - "memory-bus" 20 | build: 21 | context: . 22 | dockerfile: Dockerfile 23 | ports: 24 | - "8080:8080" 25 | environment: 26 | ApiEndpoints__ReadByteApi: http://memory-bus.:8080/api/v1/readByte 27 | ApiEndpoints__ReadWriteApi: http://memory-bus.:8080/api/v1/writeByte 28 | ApiEndpoints__ReadRangeApi: http://memory-bus.:8080/api/v1/readRange 29 | ApiEndpoints__InitialiseMemoryApi: http://memory-bus.:8080/api/v1/initialise 30 | ApiEndpoints__StartProgramApi: http://emulator-backend-8080.:8080/api/v1/start 31 | ApiEndpoints__GetStatusApi: http://emulator-backend-8080.:8080/api/v1/state 32 | networks: 33 | - space-invaders-ui-dev 34 | 35 | emulator-backend-8080: 36 | image: ghcr.io/21st-century-emulation/emulator-backend-8080:latest 37 | restart: always 38 | depends_on: 39 | - "memory-bus" 40 | networks: 41 | - space-invaders-ui-dev 42 | environment: 43 | INTERRUPT_SERVICE_URL: http://spaceinvadersui.:8080/api/v1/checkInterrupts 44 | INITIALISE_CPU_SERVICE: http://spaceinvadersui.:8080/api/v1/cpu/initialise 45 | READ_RANGE_API: http://memory-bus.:8080/api/v1/readRange 46 | ACI: http://aci.:8080/api/v1/execute 47 | ADC: http://adc.:8080/api/v1/execute 48 | ADD: http://add.:8080/api/v1/execute 49 | ADI: http://adi.:8080/api/v1/execute 50 | ANA: http://ana.:8080/api/v1/execute 51 | ANI: http://ani.:8080/api/v1/execute 52 | CALL: http://call.:8080/api/v1/execute 53 | CMA: http://cma.:8080/api/v1/execute 54 | CMC: http://cmc.:8080/api/v1/execute 55 | CMP: http://cmp.:8080/api/v1/execute 56 | CPI: http://cpi.:8080/api/v1/execute 57 | DAA: http://daa.:8080/api/v1/execute 58 | DAD: http://dad.:80/api/v1/execute 59 | DCR: http://dcr.:8080/api/v1/execute 60 | DCX: http://dcx.:8080/api/v1/execute 61 | DI: http://di.:8080/api/v1/execute 62 | EI: http://ei.:8080/api/v1/execute 63 | IN: http://spaceinvadersui.:8080/api/v1/in 64 | INR: http://inr.:8080/api/v1/execute 65 | INX: http://inx.:8080/api/v1/execute 66 | JMP: http://jmp.:8080/api/v1/execute 67 | LDA: http://lda.:8080/api/v1/execute 68 | LDAX: http://ldax.:8080/api/v1/execute 69 | LHLD: http://lhld.:8080/api/v1/execute 70 | LXI: http://lxi.:8080/api/v1/execute 71 | MOV: http://mov.:8080/api/v1/execute 72 | MVI: http://mvi.:8080/api/v1/execute 73 | NOOP: http://noop.:8080/api/v1/execute 74 | ORA: http://ora.:8080/api/v1/execute 75 | ORI: http://ori.:8080/api/v1/execute 76 | OUT: http://spaceinvadersui.:8080/api/v1/out 77 | PCHL: http://pchl.:8080/api/v1/execute 78 | POP: http://pop.:8080/api/v1/execute 79 | PUSH: http://push.:8080/api/v1/execute 80 | RAL: http://ral.:8080/api/v1/execute 81 | RAR: http://rar.:8080/api/v1/execute 82 | RET: http://ret.:8080/api/v1/execute 83 | RLC: http://rlc.:8080/api/v1/execute 84 | RRC: http://rrc.:8080/api/v1/execute 85 | RST: http://rst.:8080/api/v1/execute 86 | SBB: http://sbb.:8080/api/v1/execute 87 | SBI: http://sbi.:8080/api/v1/execute 88 | SHLD: http://shld.:8080/api/v1/execute 89 | SPHL: http://sphl.:8080/api/v1/execute 90 | STA: http://sta.:8080/api/v1/execute 91 | STAX: http://stax.:8080/api/v1/execute 92 | STC: http://stc.:8080/api/v1/execute 93 | SUB: http://sub.:8080/api/v1/execute 94 | SUI: http://sui.:8080/api/v1/execute 95 | XCHG: http://xchg.:8080/api/v1/execute 96 | XRA: http://xra.:8080/api/v1/execute 97 | XRI: http://xri.:8080/api/v1/execute 98 | XTHL: http://xthl.:8080/api/v1/execute 99 | 100 | aci: 101 | image: ghcr.io/21st-century-emulation/aci:latest 102 | ports: 103 | - "8080" 104 | environment: 105 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 106 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 107 | restart: always 108 | networks: 109 | - space-invaders-ui-dev 110 | adc: 111 | image: ghcr.io/21st-century-emulation/adc:latest 112 | ports: 113 | - "8080" 114 | environment: 115 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 116 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 117 | restart: always 118 | networks: 119 | - space-invaders-ui-dev 120 | add: 121 | image: ghcr.io/21st-century-emulation/add:latest 122 | ports: 123 | - "8080" 124 | environment: 125 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 126 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 127 | restart: always 128 | networks: 129 | - space-invaders-ui-dev 130 | adi: 131 | image: ghcr.io/21st-century-emulation/adi:latest 132 | ports: 133 | - "8080" 134 | environment: 135 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 136 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 137 | restart: always 138 | networks: 139 | - space-invaders-ui-dev 140 | ana: 141 | image: ghcr.io/21st-century-emulation/ana:latest 142 | ports: 143 | - "8080" 144 | environment: 145 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 146 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 147 | restart: always 148 | networks: 149 | - space-invaders-ui-dev 150 | ani: 151 | image: ghcr.io/21st-century-emulation/ani:latest 152 | ports: 153 | - "8080" 154 | environment: 155 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 156 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 157 | restart: always 158 | networks: 159 | - space-invaders-ui-dev 160 | call: 161 | image: ghcr.io/21st-century-emulation/call:latest 162 | ports: 163 | - "8080" 164 | environment: 165 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 166 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 167 | restart: always 168 | networks: 169 | - space-invaders-ui-dev 170 | cma: 171 | image: ghcr.io/21st-century-emulation/cma:latest 172 | ports: 173 | - "8080" 174 | environment: 175 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 176 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 177 | restart: always 178 | networks: 179 | - space-invaders-ui-dev 180 | cmc: 181 | image: ghcr.io/21st-century-emulation/cmc:latest 182 | ports: 183 | - "8080" 184 | environment: 185 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 186 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 187 | restart: always 188 | networks: 189 | - space-invaders-ui-dev 190 | cmp: 191 | image: ghcr.io/21st-century-emulation/cmp:latest 192 | ports: 193 | - "8080" 194 | environment: 195 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 196 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 197 | restart: always 198 | networks: 199 | - space-invaders-ui-dev 200 | cpi: 201 | image: ghcr.io/21st-century-emulation/cpi:latest 202 | ports: 203 | - "8080" 204 | environment: 205 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 206 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 207 | restart: always 208 | networks: 209 | - space-invaders-ui-dev 210 | daa: 211 | image: ghcr.io/21st-century-emulation/daa:latest 212 | ports: 213 | - "8080" 214 | environment: 215 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 216 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 217 | restart: always 218 | networks: 219 | - space-invaders-ui-dev 220 | dad: 221 | image: ghcr.io/21st-century-emulation/dad:latest 222 | ports: 223 | - "80" 224 | environment: 225 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 226 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 227 | restart: always 228 | networks: 229 | - space-invaders-ui-dev 230 | dcr: 231 | image: ghcr.io/21st-century-emulation/dcr:latest 232 | ports: 233 | - "8080" 234 | environment: 235 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 236 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 237 | restart: always 238 | networks: 239 | - space-invaders-ui-dev 240 | dcx: 241 | image: ghcr.io/21st-century-emulation/dcx:latest 242 | ports: 243 | - "8080" 244 | environment: 245 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 246 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 247 | restart: always 248 | networks: 249 | - space-invaders-ui-dev 250 | di: 251 | image: ghcr.io/21st-century-emulation/di:latest 252 | ports: 253 | - "8080" 254 | environment: 255 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 256 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 257 | restart: always 258 | networks: 259 | - space-invaders-ui-dev 260 | ei: 261 | image: ghcr.io/21st-century-emulation/ei:latest 262 | ports: 263 | - "8080" 264 | environment: 265 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 266 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 267 | restart: always 268 | networks: 269 | - space-invaders-ui-dev 270 | inr: 271 | image: ghcr.io/21st-century-emulation/inr:latest 272 | ports: 273 | - "8080" 274 | environment: 275 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 276 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 277 | restart: always 278 | networks: 279 | - space-invaders-ui-dev 280 | inx: 281 | image: ghcr.io/21st-century-emulation/inx:latest 282 | ports: 283 | - "8080" 284 | environment: 285 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 286 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 287 | restart: always 288 | networks: 289 | - space-invaders-ui-dev 290 | jmp: 291 | image: ghcr.io/21st-century-emulation/jmp:latest 292 | ports: 293 | - "8080" 294 | environment: 295 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 296 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 297 | restart: always 298 | networks: 299 | - space-invaders-ui-dev 300 | lda: 301 | image: ghcr.io/21st-century-emulation/lda:latest 302 | ports: 303 | - "8080" 304 | environment: 305 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 306 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 307 | restart: always 308 | networks: 309 | - space-invaders-ui-dev 310 | ldax: 311 | image: ghcr.io/21st-century-emulation/ldax:latest 312 | ports: 313 | - "8080" 314 | environment: 315 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 316 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 317 | restart: always 318 | networks: 319 | - space-invaders-ui-dev 320 | lhld: 321 | image: ghcr.io/21st-century-emulation/lhld:latest 322 | ports: 323 | - "8080" 324 | environment: 325 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 326 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 327 | restart: always 328 | networks: 329 | - space-invaders-ui-dev 330 | lxi: 331 | image: ghcr.io/21st-century-emulation/lxi:latest 332 | ports: 333 | - "8080" 334 | environment: 335 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 336 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 337 | restart: always 338 | networks: 339 | - space-invaders-ui-dev 340 | mov: 341 | image: ghcr.io/21st-century-emulation/mov:latest 342 | ports: 343 | - "8080" 344 | environment: 345 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 346 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 347 | restart: always 348 | networks: 349 | - space-invaders-ui-dev 350 | mvi: 351 | image: ghcr.io/21st-century-emulation/mvi:latest 352 | ports: 353 | - "8080" 354 | environment: 355 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 356 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 357 | restart: always 358 | networks: 359 | - space-invaders-ui-dev 360 | noop: 361 | image: ghcr.io/21st-century-emulation/noop:latest 362 | ports: 363 | - "8080" 364 | environment: 365 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 366 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 367 | restart: always 368 | networks: 369 | - space-invaders-ui-dev 370 | ora: 371 | image: ghcr.io/21st-century-emulation/ora:latest 372 | ports: 373 | - "8080" 374 | environment: 375 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 376 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 377 | restart: always 378 | networks: 379 | - space-invaders-ui-dev 380 | ori: 381 | image: ghcr.io/21st-century-emulation/ori:latest 382 | ports: 383 | - "8080" 384 | environment: 385 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 386 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 387 | restart: always 388 | networks: 389 | - space-invaders-ui-dev 390 | pchl: 391 | image: ghcr.io/21st-century-emulation/pchl:latest 392 | ports: 393 | - "8080" 394 | environment: 395 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 396 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 397 | restart: always 398 | networks: 399 | - space-invaders-ui-dev 400 | pop: 401 | image: ghcr.io/21st-century-emulation/pop:latest 402 | ports: 403 | - "8080" 404 | environment: 405 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 406 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 407 | restart: always 408 | networks: 409 | - space-invaders-ui-dev 410 | push: 411 | image: ghcr.io/21st-century-emulation/push:latest 412 | ports: 413 | - "8080" 414 | environment: 415 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 416 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 417 | restart: always 418 | networks: 419 | - space-invaders-ui-dev 420 | ral: 421 | image: ghcr.io/21st-century-emulation/ral:latest 422 | ports: 423 | - "8080" 424 | environment: 425 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 426 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 427 | restart: always 428 | networks: 429 | - space-invaders-ui-dev 430 | rar: 431 | image: ghcr.io/21st-century-emulation/rar:latest 432 | ports: 433 | - "8080" 434 | environment: 435 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 436 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 437 | restart: always 438 | networks: 439 | - space-invaders-ui-dev 440 | ret: 441 | image: ghcr.io/21st-century-emulation/ret:latest 442 | ports: 443 | - "8080" 444 | environment: 445 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 446 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 447 | restart: always 448 | networks: 449 | - space-invaders-ui-dev 450 | rlc: 451 | image: ghcr.io/21st-century-emulation/rlc:latest 452 | ports: 453 | - "8080" 454 | environment: 455 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 456 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 457 | restart: always 458 | networks: 459 | - space-invaders-ui-dev 460 | rrc: 461 | image: ghcr.io/21st-century-emulation/rrc:latest 462 | ports: 463 | - "8080" 464 | environment: 465 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 466 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 467 | restart: always 468 | networks: 469 | - space-invaders-ui-dev 470 | rst: 471 | image: ghcr.io/21st-century-emulation/rst:latest 472 | ports: 473 | - "8080" 474 | environment: 475 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 476 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 477 | restart: always 478 | networks: 479 | - space-invaders-ui-dev 480 | sbb: 481 | image: ghcr.io/21st-century-emulation/sbb:latest 482 | ports: 483 | - "8080" 484 | environment: 485 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 486 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 487 | restart: always 488 | networks: 489 | - space-invaders-ui-dev 490 | sbi: 491 | image: ghcr.io/21st-century-emulation/sbi:latest 492 | ports: 493 | - "8080" 494 | environment: 495 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 496 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 497 | restart: always 498 | networks: 499 | - space-invaders-ui-dev 500 | shld: 501 | image: ghcr.io/21st-century-emulation/shld:latest 502 | ports: 503 | - "8080" 504 | environment: 505 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 506 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 507 | restart: always 508 | networks: 509 | - space-invaders-ui-dev 510 | sphl: 511 | image: ghcr.io/21st-century-emulation/sphl:latest 512 | ports: 513 | - "8080" 514 | environment: 515 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 516 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 517 | restart: always 518 | networks: 519 | - space-invaders-ui-dev 520 | sta: 521 | image: ghcr.io/21st-century-emulation/sta:latest 522 | ports: 523 | - "8080" 524 | environment: 525 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 526 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 527 | restart: always 528 | networks: 529 | - space-invaders-ui-dev 530 | stax: 531 | image: ghcr.io/21st-century-emulation/stax:latest 532 | ports: 533 | - "8080" 534 | environment: 535 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 536 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 537 | restart: always 538 | networks: 539 | - space-invaders-ui-dev 540 | stc: 541 | image: ghcr.io/21st-century-emulation/stc:latest 542 | ports: 543 | - "8080" 544 | environment: 545 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 546 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 547 | restart: always 548 | networks: 549 | - space-invaders-ui-dev 550 | sub: 551 | image: ghcr.io/21st-century-emulation/sub:latest 552 | ports: 553 | - "8080" 554 | environment: 555 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 556 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 557 | restart: always 558 | networks: 559 | - space-invaders-ui-dev 560 | sui: 561 | image: ghcr.io/21st-century-emulation/sui:latest 562 | ports: 563 | - "8080" 564 | environment: 565 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 566 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 567 | restart: always 568 | networks: 569 | - space-invaders-ui-dev 570 | xchg: 571 | image: ghcr.io/21st-century-emulation/xchg:latest 572 | ports: 573 | - "8080" 574 | environment: 575 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 576 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 577 | restart: always 578 | networks: 579 | - space-invaders-ui-dev 580 | xra: 581 | image: ghcr.io/21st-century-emulation/xra:latest 582 | ports: 583 | - "8080" 584 | environment: 585 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 586 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 587 | restart: always 588 | networks: 589 | - space-invaders-ui-dev 590 | xri: 591 | image: ghcr.io/21st-century-emulation/xri:latest 592 | ports: 593 | - "8080" 594 | environment: 595 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 596 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 597 | restart: always 598 | networks: 599 | - space-invaders-ui-dev 600 | xthl: 601 | image: ghcr.io/21st-century-emulation/xthl:latest 602 | ports: 603 | - "8080" 604 | environment: 605 | WRITE_MEMORY_API: http://memory-bus.:8080/api/v1/writeByte 606 | READ_MEMORY_API: http://memory-bus.:8080/api/v1/readByte 607 | restart: always 608 | networks: 609 | - space-invaders-ui-dev 610 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21st Century Space Invaders 6 | 7 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
ABCDEHLPCSPIE
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
SignZeroAuxCarryParityCarry
73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 |
c.f. https://computerarcheology.com/Arcade/SpaceInvaders/RAMUse.html
AddressValueNameDescription
0x2000waitOnDrawCleared by alien-draw and set by next-alien. This ensures no alien gets missed while drawing.
0x2001
0x2002alienIsExplodingNot-0 if an alien is exploding, 0 if not exploding
0x2003expAlienTimerTime (ISR ticks) left in alien-explosion
0x2004alienRowRow number of current alien (cursor)
0x2005alienFrameAnimation frame number (0 or 1) for current alien (cursor)
0x2006alienCurIndexAlien cursor index (from 0 to 54)
0x2007refAlienDYrReference alien delta Yr
0x2008refAlienDXrReference alien deltaXr
0x2009refAlienYrReference alien Yr coordinate
0x200ArefAlienXrReference alien Xr coordinate
0x200BalienPosLSBAlien cursor bit pos (LSB)
0x200CalienPosMSBAlien cursor bit pos (MSB)
0x200DrackDirectionValue 0 if rack is moving right or 1 if rack is moving left
0x200ErackDownDeltaConstant value of alien rack dropping after bumping screen edge
0x200F
0x2010obj0TimerMSB
0x2011obj0TimerLSBWait 128 interrupts (about 2 secs) before player task starts
0x2012obj0TimerExtra
0x2013obj0HanlderLSB
0x2014oBJ0HanlderMSBPlayer handler code at 028E
0x2015playerAlivePlayer is alive (FF=alive). Toggles between 0 and 1 for blow-up images.
0x2016expAnimateTimerTime till next blow-up sprite change (reloaded to 5)
0x2017expAnimateCntNumber of changes left in blow-up sequence
0x2018plyrSprPicLPlayer sprite descriptor ... picture LSB
0x2019plyrSprPicMPlayer sprite descriptor ... picture MSB
0x201AplayerYrPlayer sprite descriptor ... location LSB
0x201BplayerXrPlayer sprite descriptor ... location MSB
0x201CplyrSprSizPlayer sprite descriptor ... size of sprite
0x201DnextDemoCmdNext movement command for demo
0x201EhidMessSeqSet to 1 after 1st of 2 sequences are entered for hidden-message display
0x201FAppears to be unused
0x2020obj1TimerMSB
0x2021obj1TimerLSB
0x2022obj1TimerExtraAll 0's ... run immediately
0x2023obj1HandlerLSB
0x2024obj1HandlerMSBShot handler code at 03BB
0x2025plyrShotStatus0 if available, 1 if just initiated, 2 moving normally, 3 hit something besides alien, 5 if alien explosion is in progress, 4 if alien has exploded (remove from active duty)
0x2026blowUpTimerSprite blow-up timer
0x2027obj1ImageLSB
0x2028obj1ImageMSBSprite image at 1C90 (just one byte)
0x2029obj1CoorYrPlayer shot Y coordinate
0x202Aobj1CoorXrPlayer shot X coordinate
0x202Bobj1ImageSizeSize of shot image (just one byte)
0x202CshotDeltaXShot's delta X
0x202DfireBounce1 if button has been handled but remains down
0x202E
0x202F
0x2030obj2TimerMSB
0x2031obj2TimerLSB
0x2032obj2TimerExtraGO-3 runs when this is 1. GO-4 runs when this is 2. (copied to 2080 in game loop)
0x2033obj2HandlerLSB
0x2034obj2HandlerMSBHandler code at 0476
0x2035rolShotStatus
0x2036rolShotStepCnt
0x2037rolShotTrackA 0 means this shot tracks the player
0x2038rolShotCFirLSBPointer to column-firing table LSB (not used for targeting)
0x2039rolShotCFirMSBPointer to column-firing table MSB (not used for MSB counter
0x203ArolShotBlowCnt
0x203BrolShotImageLSB
0x203CrolShotImageMSB
0x203DrolShotYr
0x203ErolShotXr
0x203FrolShotSize
0x2040obj3TimerMSB
0x2041obj3TimerLSB
0x2042obj3TimerExtra
0x2043obj3HandlerLSB
0x2044obj3HandlerMSBHandler code at 04B6
0x2045pluShotStatus
0x2046pluShotStepCnt
0x2047pluShotTrackA 1 means this shot does not track the player
0x2048pluShotCFirLSBPointer to column-firing table LSB
0x2049pluShotCFirMSBPointer to column-firing table MSB
0x204ApluShotBlowCnt
0x204BpluShotImageLSB
0x204CpluShotImageMSB
0x204DpluShotYr
0x204EpluSHotXr
0x204FpluShotSize
0x2050obj4TimerMSB
0x2051obj4TimerLSB
0x2052obj4TimerExtra
0x2053obj4HandlerLSB
0x2054obj4HandlerMSBHandler code at 0682
0x2055squShotStatus
0x2056squShotStepCnt
0x2057squShotTrackA 1 means this shot does not track the player
0x2058squShotCFirLSBPointer to column-firing table LSB
0x2059squShotCFirMSBPointer to column-firing table MSB
0x205AsquSHotBlowCnt
0x205BsquShotImageLSB
0x205CsquShotImageMSB
0x205DsquShotYr
0x205EsquShotXr
0x205FsquShotSize
0x2060endOfTasksFF marks the end of the tasks list
0x2061collisionSet to 1 if sprite-draw detects collision
0x2062expAlienLSB
0x2063expAlienMSBExploding alien picture 1CC0
0x2064expAlienYrY coordinate of exploding alien
0x2065expAlienXrX coordinate of exploding alien
0x2066expAlienSizeSize of exploding alien sprite (16 bytes)
0x2067playerDataMSBCurrent player's data-pointer MSB (21xx or 22xx)
0x2068playerOK1 means OK, 0 means blowing up
0x2069enableAlienFire1 means aliens can fire, 0 means not
0x206AalienFireDelayCount down till aliens can fire (2069 flag is then set)
0x206BoneAlien1 when only one alien is on screen
0x206Ctemp206CHolds the value ten ... number of characters in each "=xx POINTS" string but gets set to 18 in mem copy before game.
0x206DinvadedSet to 1 when player blows up because rack has reached bottom
0x206EskipPlungerWhen there is only one alien left this goes to 1 to disable the plunger-shot when it ends
0x206F
0x2070otherShot1When processing a shot, this holds one of the other shot's info
0x2071otherShot2When processing a shot, this holds one of the other shot's info
0x2072vblankStatus80=screen is being drawn (don't touch), 0=blanking in progress (ok to change)
0x2073aShotStatusBit 0 set if shot is blowing up, bit 7 set if active
0x2074aShotStepCntCount of steps made by shot (used for fire reload rate)
0x2075aShotTrack0 if shot tracks player or 1 if it uses the column-fire table
0x2076aShotCFirLSBPointer to column-firing table LSB
0x2077aShotCFirMSBPointer to column-firing table MSB
0x2078aShotBlowCntAlen shot blow up counter. At 3 the explosion is drawn. At 0 it is done.
0x2079aShotImageLSBAlien shot image LSB
0x207AaShotImageMSBAlien shot image MSB
0x207BalienShotYrAlien shot delta Y
0x207CalienShotXrAlien shot delta X
0x207DalienShotSizeAlien shot size
0x207EalienShotDeltaAlien shot speed. Normally -1 but set to -4 with less than 9 aliens
0x207FshotPicEndthe last picture in the current alien shot animation
0x2080shotSyncAll 3 shots are synchronized to the GO-2 timer. This is copied from timer in the game loop
0x2081tmp2081Used to hold the remember/restore flag in shield-copy routine
0x2082numAliensNumber of aliens on screen
0x2083saucerStartFlag to start saucer (set to 1 when 2091:2092 counts down to 0)
0x2084saucerActiveSaucer is on screen (1 means yes)
0x2085saucerHitSaucer has been hit (1 means draw it but don't move it)
0x2086saucerHitTimeHit-sequence timer (explosion drawn at 1F, score drawn at 18)
0x2087saucerPriLocLSBMystery ship print descriptor ... coordinate LSB
0x2088saucerPriLocMSBMystery ship print descriptor ... coordinate MSB
0x2089saucerPriPicLSBMystery ship print descriptor ... message LSB
0x208AsaucerPriPicMSBMystery ship print descriptor ... message MSB
0x208BsaucerPriSizeMystery ship print descriptor ... number of characters
0x208CsaucerDeltaYMystery ship delta Y
0x208DsauScoreLSBPointer into mystery-ship score table (MSB)
0x208EsauScoreMSBPointer into mystery-ship score table (LSB)
0x208FshotCountLSBBumped every shot-removal. Saucer's direction is bit 0. (0=2/29, 1=-2/E0)
0x2090shotCountMSBRead as two-bytes with 208F, but never used as such.
0x2091tillSaucerLSB
0x2092tillSaucerMSBCount down every game loop. When it reaches 0 saucer is triggerd. Reset to 600.
0x2093waitStartLoop1=in wait-for-start loop, 0=in splash screens
0x2094soundPort3Current status of sound port (out $03)
0x2095changeFleetSndSet to 1 in ISR if time to change the fleet sound
0x2096fleetSndCntDelay until next fleet movement tone
0x2097fleetSndReloadReload value for fleet sound counter
0x2098soundPort5Current status of sound port (out $05)
0x2099extraHoldDuration counter for extra-ship sound
0x209Atilt1 if tilt handling is in progress
0x209BfleetSndHoldTime to hold fleet-sound at each change
0x209C
0x209D
0x209E
0x209F
0x20A0
0x20A1
0x20A2
0x20A3
0x20A4
0x20A5
0x20A6
0x20A7
0x20A8
0x20A9
0x20AA
0x20AB
0x20AC
0x20AD
0x20AE
0x20AF
0x20B0
0x20B1
0x20B2
0x20B3
0x20B4
0x20B5
0x20B6
0x20B7
0x20B8
0x20B9
0x20BA
0x20BB
0x20BC
0x20BD
0x20BE
0x20BF
0x20C0isrDelayDelay counter decremented in ISR
0x20C1isrSplashTask1=In demo, 2=Little-alien and Y, 4=shooting extra 'C'
0x20C2splashAnFormImage form (increments each draw)
0x20C3splashDeltaXDelta X
0x20C4splashDeltaYDelta Y
0x20C5splashYrY coordinate
0x20C6splashXrX coordinate
0x20C7splashImageLSB
0x20C8splashImageMSBBase image 1BA0 (small alien with upside down Y)
0x20C9splashImageSizeSize of image (16 bytes)
0x20CAsplashTargetYTarget Y coordinate
0x20CBsplashReachedReached target Y flag (1 when reached)
0x20CCsplashImRestLSBBase image for restore 1BA0 is small alien with upside down Y
0x20CDsplashImRestMSB
0x20CEtwoPlayers1 for yes, 0 means 1 player
0x20CFaShotReloadRateBased on the MSB of the player's score ... how fast the aliens reload their shots
0x20D0; This is where the alien-sprite-carying-the-Y ...
0x20D1; ... lives in ROM
0x20D2
0x20D3
0x20D4
0x20D5
0x20D6
0x20D7
0x20D8
0x20D9
0x20DA
0x20DB
0x20DC
0x20DD
0x20DE
0x20DF
0x20E0
0x20E1
0x20E2
0x20E3
0x20E4
0x20E5player1ExExtra ship has been awarded = 0
0x20E6player2ExExtra ship has been awarded = 0
0x20E7player1Alive1 if player is alive, 0 if dead (after last man)
0x20E8player2Alive1 if player is alive, 0 if dead (after last man)
0x20E9suspendPlay1=game things are moving, 0=game things are suspended
0x20EAcoinSwitch1=switch down, 0=switch up (used to debounce coin switch)
0x20EBnumCoinsnumber of coin credits in BCD format (99 max)
0x20ECsplashAnimate0 for animation during splash and 1 for not. This alternates after every cycle.
0x20EDdemoCmdPtrLSBpointer to demo commands LSB 1663
0x20EEdemoCmdPtrMSBpointer to demo commands MSB
0x20EFgameMode1=game running, 0=demo or splash screens
0x20F0
0x20F1adjustScoreSet to 1 if score needs adjusting
0x20F2scoreDeltaLSBScore adjustment (LSB)
0x20F3scoreDeltaMSBScore adjustment (MSB)
0x20F4HiScorLHi-score descriptor ... value LSB
0x20F5HiScorMHi-score descriptor ... value MSB
0x20F6HiScorLoLHi-score descriptor ... location LSB
0x20F7HiScorLoMHi-score descriptor ... location MSB
0x20F8P1ScorLHi-score descriptor ... value LSB
0x20F9P1ScorMHi-score descriptor ... value MSB
0x20FAP1ScorLoLHi-score descriptor ... location LSB
0x20FBP1ScorLoMHi-score descriptor ... location MSB
0x20FCP2ScorLHi-score descriptor ... value LSB
0x20FDP2ScorMHi-score descriptor ... value MSB
0x20FEP2ScorLoLHi-score descriptor ... location LSB
0x20FFP2ScorLoMHi-score descriptor ... location MSB
341 |
342 |
343 |
344 |
345 | 346 | --------------------------------------------------------------------------------