diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..f4f4ef65 --- /dev/null +++ b/.env.local @@ -0,0 +1,52 @@ +# Environment for local development. +# Compatible with podman/docker with linux containers. + +# Database settings +DB_PASSWORD="l3/dev/null || true + +log-asset: ## Follow asset logs + ./script/compose.sh logs asset -f + +log-db: ## Follow db logs + ./script/compose.sh logs db -f + +log-server: ## Follow server logs + ./script/compose.sh logs server -f + +test-unit: ## Run the unit test tier (2) in the test container, no database required + ./script/compose.sh --profile test run --no-deps --build --rm test dotnet test src/Perpetuum.Tests/Perpetuum.Tests.csproj -c Release -p:Platform=x64 --no-build + +test-integration: ## Run the integration test tier (3) in the test container, against the live database, bringing up db + migration first (migration is idempotent and exits when already done) + ./script/compose.sh up -d db --wait + ./script/compose.sh up migration + ./script/compose.sh --profile test run --build --rm test dotnet test src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 --no-build + +test-smoke: ## Run the end-to-end smoke test (tier 1) against the docker server stack + ./script/smoke-test.sh + +check-assets: ## Verify presence and integrity of assets listed in manifest + python3 ./script/check_assets.py --check-sizes + +.PHONY: help up start stop down delete restart reset clean-cache log-asset log-db log-server test-smoke test-unit test-integration check-assets extract-models + + diff --git a/PerpetuumServer2.sln b/PerpetuumServer2.sln index 9a70b043..b33a8751 100644 --- a/PerpetuumServer2.sln +++ b/PerpetuumServer2.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35431.28 diff --git a/README.md b/README.md index 551f4039..8de180be 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ # The Open Perpetuum Server 2 +## Native Windows host + +`Perpetuum.Server` is annotated `[SupportedOSPlatform("windows")]` and the Admin Tool is WPF. You need the .NET 8 SDK, a SQL Server instance, and the `perpetuumsa` database — see [OPDB](https://github.com/OpenPerpetuum/OPDB) for restore and patches. ## Running a local server Windows and x64 only. The bootstrapper is annotated `[SupportedOSPlatform("windows")]` and the Admin @@ -79,3 +82,134 @@ dotnet run -- "C:\PerpetuumServer\data" The server is up when the log reads `>>>> Perpetuum Server State : [Online]`. Ctrl+C shuts it down; a clean shutdown ends at `State : [Off]`. +## Docker compose + +Local development runs in **Linux containers** (Docker or Podman). + +`compose.yml` defines the asset server, SQL Server, migration job, and game server. Configuration lives in `.env.local`. + +Two named volumes persist between restarts: + +- `openperpetuum-data` — original `PerpetuumServer/data`, custom layers, and a generated `perpetuum.ini` +- `openperpetuum-db` — SQL Server files + +`make` wraps the compose commands; you can call `docker compose` / `podman compose` yourself if you prefer. + +### Requirements + +- Docker or Podman (Linux containers) +- (optional) `make` +- Steam: Perpetuum Dedicated Server installed +- Latest gamma island layers: https://drive.google.com/file/d/1Xp0T1K57Pv-vjgmpXMG8Iea_ec0bWYR4/view?usp=drive_link +- Latest asset resource: https://drive.google.com/file/d/18fh8aRqMP1J7ycGBNGraFyQ31mMXZaq1/view?usp=drive_link + +### 1. Clone and submodules + +```sh +git clone https://github.com/OpenPerpetuum/PerpetuumServer2.git +# or: git clone git@github.com:OpenPerpetuum/PerpetuumServer2.git +cd PerpetuumServer2 +git submodule init && git submodule update +``` + +Submodules: + +- `db` (OPDB) — database migration files per game update +- `asset` (OPResource) — client resources served when a client connects (definitions, translations, gfx, layers, audio, custom bot models) + +### 2. Custom resources + +Do this whenever the gamma layers or asset pack are updated. + +- Uncompress the gamma layers and copy every `.bin` into both: + - `asset/lang0000/layers/GAMMA_LAYERS_NEW` + - a new `custom-layers` directory (same files) +- Unarchive the asset resource and copy `gfx`, `sfx`, and `textures` into `asset/lang0000` +- Create `perpetuum-data` and copy the Dedicated Server installer `data` folder into it (`database`, `layers`) + +Paths for `perpetuum-data` and `custom-layers` can be changed in `.env.local`. + +### 3. Configuration + +Edit `.env.local` for ports, the database password, paths, and the SQL connection string. + +The migration job writes `perpetuum.ini` from `template/perpetuum.ini.template`. Do not copy the installer `perpetuum.ini` into the data volume — that file was written for `System.Data.SqlClient` and this server uses `Microsoft.Data.SqlClient`. The template already uses a Linux-compatible string: SQL authentication (`sa`), `TrustServerCertificate=True`, no `Trusted_Connection`, and no keywords the driver refuses (`Connection Reset`, `Network Library`, `Context Connection`). + +Linux does not support distributed transactions. `.env.local` sets `DISTRIBUTED_TRANSACTIONS=false` for that reason. + +`SERVER_PORTS` must be a range of about 300 ports starting at `SERVER_PORT` (default `17700-17900`). A single mapped port is enough to log in; entering a zone then shows a black screen. + +### 4. Run the server + +```sh +make up +``` + +This builds and starts the containers and runs migrations. The command returns before the game host is fully up; wait a few minutes. + +```sh +make log-server +``` + +The server is ready for a client when you see lines such as `Unit enter to zone` or `Planthandler STOP SIGNAL received`. + +### 5. Point the client at this host + +- Open the client → **Server list** → **ADD PRIVATE SERVER** +- Name: `local` +- Address: `127.0.0.1:17700` (use `SERVER_PORT` from `.env.local` if you changed it) +- Connect, then log in with user `test` / password `test` + +The first connect can take several minutes while the asset server transfers files. + +### 6. Stop and Cache Management + +```sh +make down # stop and remove containers; keep data and db volumes +make delete # also delete the docker volumes +make reset # force re-run full migration from scratch and refresh cache +make clean-cache # delete migration snapshot backup and hash +``` + +### Database Migration & Snapshot Cache + +The migration container seeds configuration files and applies all database patches from the `db` (OPDB) submodule to the SQL Server database. + +To optimize local development startup time from ~90s down to ~2s, the migration job employs an **automatic snapshot caching mechanism** with SHA-256 change detection: + +```mermaid +flowchart TD + Start(["Start migration container"]) --> CheckDone{"/data/done exists && !FORCE_MIGRATION?"} + CheckDone -- "Yes" --> Skip(["Skip migration (0s)"]) + + CheckDone -- "No" --> SyncFiles["Sync layer assets & perpetuum.ini to /data"] + SyncFiles --> ComputeHash["Compute SHA-256 hash of all SQL patches, base .bak & scripts"] + + ComputeHash --> CheckCache{"perpetuumsa_migrated.bak exists && Hash matches?"} + + subgraph FastPath["Fast Path (Cache Hit: ~2s)"] + CheckCache -- "Yes (Cache Hit)" --> RestoreSnapshot["RESTORE DATABASE from snapshot (perpetuumsa_migrated.bak)"] + end + + subgraph SlowPath["Full Migration (First Run / Patch Changed: ~90s)"] + CheckCache -- "No (Miss / Changed / Force)" --> CreateDB["Ensure perpetuumsa DB exists"] + CreateDB --> RestoreBase["RESTORE DATABASE from Steam base perpetuumsa.bak"] + RestoreBase --> DiscoverPatches["Auto-discover patches in numerical order (Pre_Alpha_* -> Live_*)"] + DiscoverPatches --> ApplyPatches["Apply SQL scripts (live_patch_*.sql, Raw_SQL, or *.sql)"] + ApplyPatches --> AddTestAccount["Add test account (TOOL_test_account.sql)"] + AddTestAccount --> BackupSnapshot["BACKUP DATABASE to perpetuumsa_migrated.bak WITH COMPRESSION"] + BackupSnapshot --> SaveHash["Save SHA-256 hash to perpetuumsa_migrated.hash"] + end + + RestoreSnapshot --> MarkDone["touch /data/done"] + SaveHash --> MarkDone + MarkDone --> End(["Migration complete -> Game server starts"]) +``` + +#### Key Features + +- **Automated Patch Discovery**: Automatically iterates through `Pre_Alpha_*` and `Live_*` patch folders in version order. It runs consolidated patch files (`live_patch_*.sql` / `prealpha_patch_*.sql`), `Raw_SQL/*.sql`, or loose `*.sql` files, and copies any `Server/data` assets automatically. +- **Instant Restore on Volume Wipe**: When recreating containers with `make delete && make up`, the database snapshot (`perpetuum-data/database/perpetuumsa_migrated.bak`) is preserved on host disk and restored in ~2 seconds. +- **Zero-touch Invalidation**: If you modify, add, or delete any SQL patch in the `db/` submodule, the SHA-256 hash mismatch is detected automatically, triggering a full re-migration and snapshot update. +- **Clean / Force Options**: Use `make clean-cache` to delete the snapshot, or `make reset` (`FORCE_MIGRATION=true`) to force a fresh re-migration from the raw Steam base backup. + diff --git a/asset b/asset new file mode 160000 index 00000000..bcfcf409 --- /dev/null +++ b/asset @@ -0,0 +1 @@ +Subproject commit bcfcf409ddb20e6ce741df2346edcf22a02308e2 diff --git a/compose.yml b/compose.yml new file mode 100644 index 00000000..fc1d9da2 --- /dev/null +++ b/compose.yml @@ -0,0 +1,135 @@ +services: + asset: + build: + context: ./asset + dockerfile: ../docker/Dockerfile.asset.dev + restart: unless-stopped + networks: + - dev_env + volumes: + - .:/asset + ports: + - ${ASSET_PORT}:1337 + + # One-shot init service to ensure the database volume is owned by the non-root mssql user (UID 10001) + db-init: + image: busybox + restart: "no" + command: chown -R 10001:0 /var/opt/mssql + volumes: + - openperpetuum-db:/var/opt/mssql + + db: + image: mcr.microsoft.com/mssql/server:2025-latest + restart: unless-stopped + # UID 10001 is the default non-root 'mssql' service account in the Microsoft SQL Server image. + # GID 0 is the root group required for internal file permissions in the container. + user: "10001:0" + depends_on: + db-init: + condition: service_completed_successfully + entrypoint: + - /bin/bash + - -c + - | + # Background sleep watchdog: detects host clock jump on resume and restarts sqlservr to prevent scheduler spin + # This fixes an issue when running this container on linux pinning 1 core to 100% when waking up from sleep + ( + while true; do + t1=$$(date +%s) + sleep 2 + t2=$$(date +%s) + delta=$$((t2 - t1)) + if [ "$$delta" -gt 8 ]; then + echo "==> Host resume from sleep detected (clock jumped by $${delta}s). Restarting sqlservr..." + pkill -TERM -f sqlservr || kill 1 + break + fi + done + ) & + exec /opt/mssql/bin/launch_sqlservr.sh /opt/mssql/bin/sqlservr + ports: + - ${DB_PORT}:1433 + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=${DB_PASSWORD} + - MSSQL_MEMORY_LIMIT_MB=${DB_MEMORY_LIMIT_MB:-4096} + networks: + - dev_env + healthcheck: + test: /opt/mssql-tools18/bin/sqlcmd -S localhost -C -U sa -P "${DB_PASSWORD}" -Q "SELECT 1" -b -o /dev/null + interval: 10s + timeout: 3s + retries: 10 + hostname: db + volumes: + - openperpetuum-db:/var/opt/mssql + # Mount perpetuum database path that contains the perpetuumsa.bak + - "${PERPETUUM_DATA}/database:/data" + + migration: + build: + context: . + dockerfile: ./docker/Dockerfile.migration.dev + args: + ASSET_URL: ${ASSET_URL} + CONNECTION_STRING: ${CONNECTION_STRING} + SERVER_PORT: ${SERVER_PORT} + depends_on: + db: + condition: service_healthy + environment: + DB_PASSWORD: ${DB_PASSWORD} + FORCE_MIGRATION: ${FORCE_MIGRATION} + networks: + - dev_env + volumes: + - ./db:/migration + - "${PERPETUUM_DATA}:/base-data" + - "${CUSTOM_LAYERS}:/custom-layers" + - "./src/Perpetuum.ServerService2/data:/perpetuum-service-data" + - openperpetuum-data:/data + + server: + build: + context: . + dockerfile: ./docker/Dockerfile.server.dev + args: + RUNTIME_IDENTIFIER: ${RUNTIME_IDENTIFIER} + restart: unless-stopped + networks: + - dev_env + ports: + - ${SERVER_PORTS}:17700-17900 + environment: + GameRoot: ${GAME_ROOT} + DistributedTransactions: ${DISTRIBUTED_TRANSACTIONS} + depends_on: + migration: + condition: service_completed_successfully + volumes: + - openperpetuum-data:/data + + + # Test runner (unit + integration tiers). Excluded from the default stack via the "test" profile: + # make test-unit / make test-integration + # or manually: docker compose --profile test run --rm test + test: + profiles: [test] + build: + context: . + dockerfile: ./docker/Dockerfile.test + args: + SERVER_PORT: ${SERVER_PORT} + ASSET_URL: ${ASSET_URL} + CONNECTION_STRING: ${CONNECTION_STRING} + networks: + - dev_env + +networks: + dev_env: + driver: bridge + +volumes: + openperpetuum-data: + openperpetuum-db: diff --git a/db b/db new file mode 160000 index 00000000..64db10cf --- /dev/null +++ b/db @@ -0,0 +1 @@ +Subproject commit 64db10cfd021e5917f02ce2cd1c4b56b0dffee6e diff --git a/docker/Dockerfile.asset.dev b/docker/Dockerfile.asset.dev new file mode 100644 index 00000000..e7323376 --- /dev/null +++ b/docker/Dockerfile.asset.dev @@ -0,0 +1,11 @@ +# Development dockerfile for the asset + +FROM node:alpine3.23 + +WORKDIR /var/www/OPResource + +COPY . . + +RUN npm install . + +CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/Dockerfile.migration.dev b/docker/Dockerfile.migration.dev new file mode 100644 index 00000000..a488603d --- /dev/null +++ b/docker/Dockerfile.migration.dev @@ -0,0 +1,45 @@ +FROM ubuntu:22.04 + +ARG ASSET_URL +ARG CONNECTION_STRING +ARG SERVER_PORT + +# Install dependencies for mssql-tools18 +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + apt-transport-https \ + && rm -rf /var/lib/apt/lists/* + +# Add Microsoft repository +RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | tee /etc/apt/sources.list.d/msprod.list + +# Install mssql-tools18 +RUN apt-get update && ACCEPT_EULA=Y apt-get install -y \ + mssql-tools18 \ + && rm -rf /var/lib/apt/lists/* + +# Add sqlcmd to PATH +ENV PATH="/opt/mssql-tools18/bin:${PATH}" + +WORKDIR /work + +COPY script/migration.sh . +COPY template/perpetuum.ini.template . +COPY template/restore_DB_to_original_state.sql . +COPY template/restore_migrated_DB.sql . + +RUN mv perpetuum.ini.template perpetuum.ini + +RUN sed -i "s/{SERVER_PORT}/${SERVER_PORT}/" perpetuum.ini +# Update the connection string but hides the command to hide the password +RUN set +x && \ + sed -i "s/{CONNECTION_STRING}/${CONNECTION_STRING}/" perpetuum.ini && \ + set -x +# Using # delimiter to avoid conflict with / from the url +RUN sed -i "s#{ASSET_URL}#${ASSET_URL}#" perpetuum.ini + + + +ENTRYPOINT ["/work/migration.sh"] \ No newline at end of file diff --git a/docker/Dockerfile.server.dev b/docker/Dockerfile.server.dev new file mode 100644 index 00000000..d2104611 --- /dev/null +++ b/docker/Dockerfile.server.dev @@ -0,0 +1,33 @@ +# Development dockerfile for PerpetuumServer2 + +# Stage 1: Build the project +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build + +ARG RUNTIME_IDENTIFIER + +WORKDIR /build + +COPY src ./src/ + +# Dedicated restore command to cache the layer for faster container boot time= +RUN dotnet restore src/Perpetuum.ServerService2/Perpetuum.ServerService2.csproj + +# Build the project +# Provides a self-contained executable (no dotnet runtime required) +RUN dotnet publish --force src/Perpetuum.ServerService2/Perpetuum.ServerService2.csproj --configuration Release --self-contained --runtime $RUNTIME_IDENTIFIER -o out + +# Stage 2: Runtime image +# Using runtime-deps, it includes the dependencies for dotnet without the runtime. +FROM mcr.microsoft.com/dotnet/runtime-deps:10.0 + +# Install the missing GSSAPI/Kerberos dependency used by the SQL client +RUN apt-get update && apt-get install -y libgssapi-krb5-2 curl && rm -rf /var/lib/apt/lists/* + +# Install GDI+ +RUN curl https://raw.githubusercontent.com/stulzq/awesome-dotnetcore-image/master/install/ubuntu.sh|sh + +WORKDIR /runtime + +COPY --from=build /build/out . + +ENTRYPOINT ["/runtime/Perpetuum.ServerService2"] \ No newline at end of file diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test new file mode 100644 index 00000000..38b0462a --- /dev/null +++ b/docker/Dockerfile.test @@ -0,0 +1,42 @@ +# Test dockerfile for PerpetuumServer2 +# Builds both test tiers once, then reuses the image to run unit or integration tests: +# docker compose --profile test run --rm test +# +# Uses the SDK 8 image so the net8.0 test host runs without roll-forward. + +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +# Install the missing GSSAPI/Kerberos dependency used by the SQL client and fontconfig for SkiaSharp +RUN apt-get update && apt-get install -y libgssapi-krb5-2 libfontconfig1 && rm -rf /var/lib/apt/lists/* + +WORKDIR /repo + +COPY Directory.Build.targets ./ +COPY PerpetuumServer2.sln ./ +COPY src ./src +# The schema conformance tests locate the repository root via the .sln and read the +# documented object names from docs/db_structure +COPY docs/db_structure ./docs/db_structure + +ARG SERVER_PORT=17700 +ARG ASSET_URL=http://localhost:16999 +ARG CONNECTION_STRING + +# Pre-build both tiers so `dotnet test --no-build` is fast and build failures surface at image build time +RUN dotnet build src/Perpetuum.Tests/Perpetuum.Tests.csproj -c Release -p:Platform=x64 && \ + dotnet build src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 + +# Game root for the integration tier: perpetuum.ini with the database connection string, +# same pattern as Dockerfile.migration.dev (the password ends up in the image layers, +# same trade-off as the migration image). +COPY template/perpetuum.ini.template /data/perpetuum.ini + +RUN sed -i "s/{SERVER_PORT}/${SERVER_PORT}/" /data/perpetuum.ini && \ + sed -i "s#{ASSET_URL}#${ASSET_URL}#" /data/perpetuum.ini && \ + sed -i "s/{CONNECTION_STRING}/${CONNECTION_STRING}/" /data/perpetuum.ini + +ENV PERPETUUM_GAMEROOT=/data +ENV GameRoot=/data + +# Default command: unit tier +CMD ["dotnet", "test", "src/Perpetuum.Tests/Perpetuum.Tests.csproj", "-c", "Release", "-p:Platform=x64", "--no-build"] diff --git a/docs/backlog/improvements.md b/docs/backlog/improvements.md index ea5cd43e..7286139e 100644 --- a/docs/backlog/improvements.md +++ b/docs/backlog/improvements.md @@ -64,6 +64,10 @@ Remaining stages, in order: ### Notes +- **Containerized tiers 2 and 3.** Both automated tiers run in one test container (compose service + `test`, profile `test`, `docker/Dockerfile.test`) via `make test-unit` / `make test-integration`. + No local dotnet SDK or `GameRoot` setup is needed: the image pre-builds the tests and generates + `perpetuum.ini` from the same template the migration service uses. - **No production code changes.** The four existing static service locators (`Logger.Current`, `Db.DbQueryFactory`, `EntityDefault.Reader`, `Entity.Services`) turned out to be sufficient seams for everything in stages 0-4. If a later stage genuinely cannot be tested without a new seam, that is diff --git a/docs/codebase/COVERAGE.md b/docs/codebase/COVERAGE.md new file mode 100644 index 00000000..642f2cbd --- /dev/null +++ b/docs/codebase/COVERAGE.md @@ -0,0 +1,111 @@ +# Code Coverage + +**Analysis Date:** 2026-09-10 + +## Scope + +This document records the automated code coverage metrics for the backend and server codebase (`src/`), excluding the standalone client directory (`client/`). + +Measurements were performed inside the SDK test container using `dotnet-coverage` (Microsoft code coverage tools) and `ReportGenerator` against the net8.0 test suite. + +--- + +## Executive Summary + +| Scope | Line Coverage % | Covered / Coverable Lines | Branch Coverage % | Method Coverage % | Total Code Lines | +|---|---|---|---|---|---| +| **Production Code (CI Unit Tests)** | **9.2%** | 6,882 / 74,118 | **3.7%** (870 / 23,075) | **3.6%** (436 / 11,891) | 166,665 | +| **Production Code (Unit + Integration Tests)** | **9.5%** | 7,049 / 74,118 | **3.9%** (906 / 23,075) | **4.0%** (481 / 11,891) | 166,665 | +| **All Instrumented Assemblies (incl. Unit Test Suite)** | **11.3%** | 8,653 / 75,974 | **4.6%** (1,094 / 23,311) | **5.6%** (693 / 12,229) | 170,489 | +| **Full C# Solution (all 9 non-test projects in `src/`)** | **~8.3%** | 7,049 / ~85,000 | **~3.4%** | **~3.5%** | ~204,000 | + +--- + +## Per-Assembly Breakdown + +### Production Assemblies + +| Project / Assembly | Line Coverage | Branch Coverage | Source Files | Lines of Code | Description / Coverage Focus | +|---|---|---|---|---|---| +| `Perpetuum` | **11.0%** | **4.4%** | 1,337 files | ~142,800 | Core domain logic. Covered areas: geometry (`Area`, `PointExtensions`), data access fakes (`Db`, `DbQuery`), logging, bitmaps/layers. | +| `Perpetuum.RequestHandlers` | **0.4%** | **0.0%** | 585 files | ~26,500 | 200+ command handlers. Largely untested in isolation at the unit level; orchestrate zone/service logic. | +| `Perpetuum.ExportedTypes` | **0.0%** | **0.0%** | 14 files | ~7,670 | Enums, category flags, definition IDs, aggregate field constants. Declarative data without complex logic. | +| `Perpetuum.Bootstrapper` | **0.0%** | **0.0%** | 24 files | ~3,430 | Autofac dependency injection modules and container bootstrap wiring. | +| `Perpetuum.AdminTool` | **0.0%** | **0.0%** | 234 files | ~16,880 | WinForms / WPF server administration tool. | +| `Perpetuum.Server` | **0.0%** | **0.0%** | 1 file | ~110 | Console server entry point (`Program.cs`). | +| `Perpetuum.ServerService2` | **0.0%** | **0.0%** | 2 files | ~120 | Windows Service host entry point. | +| `Open.Nat` | **0.0%** | **0.0%** | 35 files | ~3,850 | Third-party UPnP / NAT-PMP traversal library. | + +### Test Assemblies + +| Test Project | Line Coverage | Tests Count | Lines of Code | Role | +|---|---|---|---|---| +| `Perpetuum.Tests` | **95.3%** | 209 unit tests | ~3,840 | Tier 2 automated unit test suite, test seams, in-memory recording fakes. | +| `Perpetuum.Tests.Integration` | **N/A** | 15 integration tests | ~820 | Tier 3 schema conformance & live DB queries. | + +--- + +## Subsystem Coverage Map + +### Well-Covered Subsystems (>80% coverage) +- **Math & Spatial Utilities:** `Area`, `PointExtensions`, `SizeExtensions`, `SpatialCollectionsSkia`. +- **Data Query Abstraction:** `DbQuery`, `DbConnectionManager`, `ConnectionStringSupport`, recording fake ADO.NET providers. +- **Guard & Validation Extensions:** `Guard.cs`, `ValueTypeExtensions.cs`. +- **Image & Layer Utilities:** `BitmapExtensions`, `BinaryStreamSkia`, `LayerExtensions`. +- **Known Regression Guards:** `Issue033EmptyFlockTests`, `Issue039InsuranceTransactionTests`. + +### Untested Subsystems (0% - 5% coverage) +- **Entity System (`src/Perpetuum/EntityFramework/`):** `Entity`, `EntityDefault`, `EntityDynamicProperties`, entity factories. +- **Module State Machines (`src/Perpetuum/Modules/`):** `ActiveModule.States`, combat equipment state transitions. +- **Request Handlers (`src/Perpetuum.RequestHandlers/`):** Over 200 client command handlers. +- **Zone Runtime & Threading (`src/Perpetuum/Zones/`, `src/Perpetuum/Threading/`):** `ProcessManager`, `Zone`, `ZoneSession`, movement, combat loops, NPC AI. +- **Game Services (`src/Perpetuum/Services/`):** `MissionEngine`, `SeasonService`, `MarketEngine`, `ProductionEngine`, `Looting`, `Social`, `Mail`. + +--- + +## Reproducing Coverage Reports + +Code coverage can be generated in the test Docker container using the commands below: + +### Generate Production Coverage (Excluding Tests) + +```bash +./script/compose.sh --profile test run --no-deps --rm test sh -c ' +export PATH="$PATH:/root/.dotnet/tools" +dotnet tool install -g dotnet-coverage >/dev/null 2>&1 || true +dotnet tool install -g dotnet-reportgenerator-globaltool >/dev/null 2>&1 || true + +dotnet-coverage collect "dotnet test src/Perpetuum.Tests/Perpetuum.Tests.csproj -c Release -p:Platform=x64 --no-build" \ + -f cobertura -o /tmp/coverage.cobertura.xml + +reportgenerator -reports:/tmp/coverage.cobertura.xml \ + -targetdir:/tmp/CoverageReport \ + -reporttypes:"TextSummary;Html" \ + -assemblyfilters:"-Perpetuum.Tests;-Perpetuum.Tests.*" + +cat /tmp/CoverageReport/Summary.txt +' +``` + +### Generate Merged Unit + Integration Coverage + +```bash +./script/compose.sh --profile test run --no-deps --rm test sh -c ' +export PATH="$PATH:/root/.dotnet/tools" +dotnet tool install -g dotnet-coverage >/dev/null 2>&1 || true +dotnet tool install -g dotnet-reportgenerator-globaltool >/dev/null 2>&1 || true + +dotnet-coverage collect "dotnet test src/Perpetuum.Tests/Perpetuum.Tests.csproj -c Release -p:Platform=x64 --no-build" \ + -f cobertura -o /tmp/cov_unit.cobertura.xml + +dotnet-coverage collect "dotnet test src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 --no-build" \ + -f cobertura -o /tmp/cov_int.cobertura.xml + +reportgenerator -reports:/tmp/cov_*.cobertura.xml \ + -targetdir:/tmp/CoverageReportMerged \ + -reporttypes:"TextSummary" \ + -assemblyfilters:"-Perpetuum.Tests;-Perpetuum.Tests.*" + +cat /tmp/CoverageReportMerged/Summary.txt +' +``` diff --git a/docs/codebase/TESTING.md b/docs/codebase/TESTING.md index 5822d84c..518a2aa0 100644 --- a/docs/codebase/TESTING.md +++ b/docs/codebase/TESTING.md @@ -5,13 +5,13 @@ ## Current State The repository has an automated test suite in three tiers. It does not cover the whole codebase — the -coverage map below states what is covered and what is not. +coverage map below states what is covered and what is not. Detailed line and branch coverage metrics are documented in [`docs/codebase/COVERAGE.md`](COVERAGE.md). | Tier | Project | Count | Needs | |------|---------|-------|-------| | 1 — smoke | `tools/smoke-test.ps1` | 1 end-to-end run | A configured `GameRoot` and a live database | -| 2 — unit | `src/Perpetuum.Tests` | 99 tests | Nothing. Runs anywhere the solution builds | -| 3 — integration | `src/Perpetuum.Tests.Integration` | 10 tests | A configured `GameRoot` and a live database | +| 2 — unit | `src/Perpetuum.Tests` | 229 tests | Nothing. Runs anywhere the solution builds | +| 3 — integration | `src/Perpetuum.Tests.Integration` | 17 tests | A configured `GameRoot` and a live database | Tier 2 is the tier that runs in CI. Tiers 1 and 3 run on a developer machine that already has the standard server environment, and skip rather than fail when it is absent. @@ -31,12 +31,36 @@ set PERPETUUM_GAMEROOT=C:\PerpetuumServer\data dotnet test src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 ``` -Tier 1, a full server run: +### Docker (docker compose) + +Tiers 2 and 3 also run in a single test container (compose service `test`, profile `test`), with the +tests pre-built at image build time and the connection string generated from `template/perpetuum.ini.template` +in `docker/Dockerfile.test`: + +```bash +make test-unit # tier 2, no database required +make test-integration # tier 3, brings up db + migration, then runs against the live DB +make test-smoke # tier 1, brings up full server stack, asserts on startup/shutdown logs +``` + +The test profile is excluded from the default `up`/`down` stack. Note that tier 3 failures about +documented objects being absent from the database (e.g. `usp_RecalculateInsurancePrices`) are real schema +drift findings, not environment errors — the test suite reports what +`docs/db_structure/` claims against what the live `perpetuumsa` contains. + +Tier 1 on Windows (bare-metal server run): ```bash pwsh tools/smoke-test.ps1 -GameRoot C:\PerpetuumServer\data ``` +Tier 1 on Linux / Docker: + +```bash +make test-smoke +# or manually: ./script/smoke-test.sh +``` + ### Environment variables | Variable | Read by | Effect | @@ -100,6 +124,13 @@ Two things are covered: Isolation is by read-only default: writes require `PERPETUUM_TESTDB_ALLOW_WRITE=1`. Tests use a single connection, because a second concurrent connection inside a `TransactionScope` escalates to MSDTC. +`make test-integration` does not set the flag, so write tests skip there (and in CI). To run them, +inject the variable into the test container: + +```bash +./script/compose.sh --profile test run --build --rm -e PERPETUUM_TESTDB_ALLOW_WRITE=1 test \ + dotnet test src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 --no-build +``` ### Tier 1 — smoke diff --git a/script/check_assets.py b/script/check_assets.py new file mode 100755 index 00000000..e0f566f5 --- /dev/null +++ b/script/check_assets.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +""" +OpenPerpetuum Asset Integrity & Presence Checker +------------------------------------------------ +Verifies that all client and server assets listed in the resource manifest +(resource_0.dat / resource_*.dat) are actually present on disk and match +expected specifications (sizes, line endings, layer binary sources). + +Usage: + python3 script/check_assets.py [OPTIONS] + +Options: + --manifest PATH Path to manifest file (default: asset/resource_0.dat) + --asset-dir PATH Path to asset root directory (default: asset) + --layers-dir PATH Additional directory to search for raw layer .bin files + (e.g., custom-layers, perpetuum-data/layers) + --check-sizes Validate file sizes against sizes recorded in manifest + --check-definitions Cross-reference definitionProperties against asset list + --missing-only Only display missing or mismatched assets + --json Output results as JSON + --no-color Disable ANSI color codes in output + --help, -h Show this help message +""" + +import argparse +import json +import os +import re +import sys + + +class Colors: + def __init__(self, enabled=True): + self.enabled = enabled + + def _wrap(self, code, text): + if not self.enabled or not sys.stdout.isatty(): + return text + return f"\033[{code}m{text}\033[0m" + + def green(self, text): + return self._wrap("32", text) + + def red(self, text): + return self._wrap("31", text) + + def yellow(self, text): + return self._wrap("33", text) + + def cyan(self, text): + return self._wrap("36", text) + + def bold(self, text): + return self._wrap("1", text) + + def gray(self, text): + return self._wrap("90", text) + + +def parse_genxy_dict(text): + """ + Parses Genxy formatted dictionary / table into Python dict. + Supports nested brackets and key=value pairs. + """ + pos = 0 + length = len(text) + + def skip_ws(): + nonlocal pos + while pos < length and text[pos] in " \t\r\n": + pos += 1 + + def parse_dict(): + nonlocal pos + res = {} + while pos < length: + skip_ws() + if pos >= length: + break + if text[pos] == "]": + pos += 1 + break + if text[pos] in "#|": + pos += 1 + skip_ws() + key_start = pos + while pos < length and text[pos] not in "= \t\r\n]": + pos += 1 + key = text[key_start:pos].strip() + skip_ws() + if pos < length and text[pos] == "=": + pos += 1 + skip_ws() + if pos < length and text[pos] == "[": + pos += 1 + val = parse_dict() + else: + val_start = pos + while pos < length and text[pos] not in "|\r\n]": + pos += 1 + val = text[val_start:pos].strip() + if key: + res[key] = val + else: + pos += 1 + return res + + return parse_dict() + + +def parse_manifest(manifest_path): + """Loads and parses resource_0.dat manifest.""" + if not os.path.isfile(manifest_path): + raise FileNotFoundError(f"Manifest file not found: {manifest_path}") + + with open(manifest_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + parsed = parse_genxy_dict(content) + resources = parsed.get("resources", parsed) + return resources + + +def parse_manifest_size(size_str): + """Parses manifest size string ('i48d8' -> hex, 'n100' -> decimal, '100' -> decimal).""" + if not size_str: + return None + size_str = size_str.strip() + try: + if size_str.startswith("i"): + return int(size_str[1:], 16) + elif size_str.startswith("n"): + return int(size_str[1:], 10) + else: + return int(size_str, 10) + except ValueError: + return None + + +def find_layer_bin(layer_rel_path, search_dirs): + """ + Given a layer resource path (e.g. lang0000/layers/0070/altitude0070/00000000.dat), + checks if a corresponding raw bin file (e.g. altitude.0070.bin) exists. + """ + m = re.match(r".*layers/(\d{4})/([a-z]+)\1/.*\.dat", layer_rel_path) + if not m: + return None, None + zone, layer = m.group(1), m.group(2) + bin_name = f"{layer}.{zone}.bin" + + for directory in search_dirs: + if not directory or not os.path.isdir(directory): + continue + candidate = os.path.join(directory, bin_name) + if os.path.isfile(candidate): + return candidate, bin_name + + return None, bin_name + + +def check_definitions_cross_reference(def_path, known_assets, asset_dir): + """ + Inspects definitionProperties/000000XX.txt to ensure all referenced + icons, models (gfx), and audio (sfx) exist. + """ + if not os.path.isfile(def_path): + return None + + with open(def_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + referenced_icons = set() + referenced_gfx = set() + referenced_sfx = set() + + for m in re.finditer(r"\|icon=\$?([a-zA-Z0-9_\-]+)", content): + referenced_icons.add(m.group(1)) + + for m in re.finditer(r"\|miniicon=\$?([a-zA-Z0-9_\-]+)", content): + referenced_icons.add(m.group(1)) + + for m in re.finditer(r"\|(?:gfxresource|resource)=\$?([a-zA-Z0-9_\-]+)", content): + referenced_gfx.add(m.group(1)) + + for m in re.finditer(r"\|(?:basesound|terrainsound|sound)=\$?([a-zA-Z0-9_\-]+)", content): + referenced_sfx.add(m.group(1)) + + missing_defs = { + "icons": [icon for icon in sorted(referenced_icons) if icon not in known_assets], + "gfx": [gfx for gfx in sorted(referenced_gfx) if gfx not in known_assets], + "sfx": [sfx for sfx in sorted(referenced_sfx) if sfx not in known_assets], + } + return missing_defs + + +def run_checks(manifest_path, asset_dir, layer_search_dirs, check_sizes=False, check_defs=False): + resources = parse_manifest(manifest_path) + + results = { + "manifest_path": os.path.abspath(manifest_path), + "total_entries": len(resources), + "present": [], + "missing": [], + "layer_bins": [], + "size_mismatches": [], + "categories": {}, + "definition_cross_ref": None, + } + + known_asset_names = set(resources.keys()) + + for name, item in resources.items(): + if not isinstance(item, dict): + continue + + raw_path = item.get("path", "") + rel_path = raw_path[1:] if raw_path.startswith("$") else raw_path + version = item.get("version", "").lstrip("n") + manifest_size = parse_manifest_size(item.get("size")) + hash_val = item.get("hash", "").lstrip("i") + + # Determine category from path + parts = rel_path.split("/") + cat = parts[1] if len(parts) > 1 else (parts[0] if parts else "other") + results["categories"].setdefault(cat, {"total": 0, "present": 0, "missing": 0}) + results["categories"][cat]["total"] += 1 + + direct_path = os.path.join(asset_dir, rel_path) + + if os.path.isfile(direct_path): + actual_size = os.path.getsize(direct_path) + status = "present" + size_ok = True + crlf_match = False + + if check_sizes and manifest_size is not None: + if actual_size != manifest_size: + # Check if CRLF line ending difference + if direct_path.endswith((".txt", ".json", ".dat", ".xml")): + try: + with open(direct_path, "rb") as tf: + raw_bytes = tf.read() + crlf_size = len( + raw_bytes.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n") + ) + if crlf_size == manifest_size: + crlf_match = True + else: + size_ok = False + except Exception: + size_ok = False + else: + size_ok = False + + item_info = { + "name": name, + "path": rel_path, + "disk_path": direct_path, + "category": cat, + "version": version, + "actual_size": actual_size, + "manifest_size": manifest_size, + "crlf_match": crlf_match, + "hash": hash_val, + } + + results["present"].append(item_info) + results["categories"][cat]["present"] += 1 + + if not size_ok: + results["size_mismatches"].append(item_info) + + elif "layers/" in rel_path: + # Check if raw .bin source exists for on-the-fly streaming + bin_path, bin_name = find_layer_bin(rel_path, layer_search_dirs) + if bin_path: + item_info = { + "name": name, + "path": rel_path, + "bin_source": bin_path, + "bin_name": bin_name, + "category": cat, + "version": version, + } + results["layer_bins"].append(item_info) + results["categories"][cat]["present"] += 1 + else: + item_info = { + "name": name, + "path": rel_path, + "category": cat, + "version": version, + "expected_bin": bin_name, + "reason": f"Neither static .dat nor raw layer bin ({bin_name}) found", + } + results["missing"].append(item_info) + results["categories"][cat]["missing"] += 1 + else: + item_info = { + "name": name, + "path": rel_path, + "disk_path": direct_path, + "category": cat, + "version": version, + "manifest_size": manifest_size, + "reason": "File not found on disk", + } + results["missing"].append(item_info) + results["categories"][cat]["missing"] += 1 + + if check_defs: + # Look for the definitionProperties file referenced in manifest or latest in folder + def_file = os.path.join(asset_dir, "lang0000/definitionProperties/00000031.txt") + if not os.path.isfile(def_file): + # Find highest numbered file + dp_dir = os.path.join(asset_dir, "lang0000/definitionProperties") + if os.path.isdir(dp_dir): + files = sorted(os.listdir(dp_dir)) + if files: + def_file = os.path.join(dp_dir, files[-1]) + + results["definition_cross_ref"] = check_definitions_cross_reference( + def_file, known_asset_names, asset_dir + ) + + return results + + +def print_report(results, colors, missing_only=False, check_sizes=False, check_defs=False): + c = colors + print(f"\n{c.bold('=== OpenPerpetuum Asset Integrity & Presence Report ===')}\n") + print(f"Manifest: {c.cyan(results['manifest_path'])}") + print(f"Total entries in manifest: {c.bold(str(results['total_entries']))}\n") + + # Category Summary Table + print(f"{'Category':<24} | {'Total':<8} | {'Present / Bin':<15} | {'Missing':<8}") + print("-" * 62) + for cat, stats in sorted(results["categories"].items()): + total = stats["total"] + pres = stats["present"] + miss = stats["missing"] + miss_str = c.red(str(miss)) if miss > 0 else c.green("0") + pres_str = c.green(str(pres)) + print(f"{cat:<24} | {total:<8} | {pres_str:<24} | {miss_str:<8}") + print("-" * 62) + + total_pres = len(results["present"]) + len(results["layer_bins"]) + total_miss = len(results["missing"]) + print( + f"{c.bold('SUMMARY')}: {c.green(f'{total_pres} available')} " + f"({len(results['present'])} direct files, {len(results['layer_bins'])} layer bin sources), " + f"{c.red(f'{total_miss} missing') if total_miss > 0 else c.green('0 missing')}" + ) + + if check_sizes: + mismatches = len(results["size_mismatches"]) + crlf_matches = sum(1 for m in results["present"] if m.get("crlf_match")) + if mismatches > 0: + print(f"Size validation: {c.yellow(f'{mismatches} mismatches detected')}") + else: + print(f"Size validation: {c.green('All checked file sizes match manifest perfectly')}") + if crlf_matches > 0: + print( + f" {c.gray(f'({crlf_matches} text files matched after CRLF/LF line-ending normalization)')}" + ) + + # Missing items breakdown + if results["missing"]: + print(f"\n{c.bold(c.red('Missing Assets:'))}") + by_cat = {} + for item in results["missing"]: + by_cat.setdefault(item["category"], []).append(item) + + for cat, items in sorted(by_cat.items()): + print(f"\n {c.bold(c.yellow(f'[{cat.upper()}] ({len(items)} missing)'))}") + for item in items[:15]: + name = item["name"] + path = item["path"] + reason = item.get("reason", "") + print(f" - {c.bold(name)} ({c.gray(path)}) -> {reason}") + if len(items) > 15: + print(f" {c.gray(f'... and {len(items) - 15} more {cat} entries')}") + + # Size mismatches breakdown + if check_sizes and results["size_mismatches"]: + print(f"\n{c.bold(c.yellow('Size Mismatches:'))}") + for item in results["size_mismatches"][:15]: + print( + f" - {item['name']} ({item['path']}): " + f"actual {item['actual_size']} bytes vs manifest {item['manifest_size']} bytes" + ) + + # Definition cross reference report + if check_defs and results.get("definition_cross_ref"): + dx = results["definition_cross_ref"] + missing_dx_count = len(dx["icons"]) + len(dx["gfx"]) + len(dx["sfx"]) + print(f"\n{c.bold('Definition Properties Cross-Check:')}") + if missing_dx_count == 0: + print(f" {c.green('All referenced icons, gfx models, and sfx are declared in manifest.')}") + else: + if dx["icons"]: + count = len(dx['icons']) + sample = ', '.join(dx['icons'][:10]) + print( + f" - {c.yellow(f'Referenced icons missing in manifest ({count}):')} {sample}..." + ) + if dx["gfx"]: + count = len(dx['gfx']) + sample = ', '.join(dx['gfx'][:10]) + print( + f" - {c.yellow(f'Referenced gfx missing in manifest ({count}):')} {sample}..." + ) + if dx["sfx"]: + count = len(dx['sfx']) + sample = ', '.join(dx['sfx'][:10]) + print( + f" - {c.yellow(f'Referenced sfx missing in manifest ({count}):')} {sample}..." + ) + + # Remediation recommendations + if total_miss > 0: + print(f"\n{c.bold(c.cyan('Remediation Advice:'))}") + has_missing_layers = any(item["category"] == "layers" for item in results["missing"]) + has_missing_icons = any(item["category"] == "icons" for item in results["missing"]) + has_missing_gfx_sfx = any(item["category"] in ["gfx", "sfx", "textures"] for item in results["missing"]) + + if has_missing_layers: + print( + f" * {c.bold('Gamma Layers Missing')}: Download the latest gamma island layers zip and unpack `.bin` files into:" + ) + print(" - `asset/lang0000/layers/GAMMA_LAYERS_NEW/`") + print(" - `custom-layers/`") + if has_missing_icons or has_missing_gfx_sfx: + print( + f" * {c.bold('Asset Resource Pack Missing')}: Download the asset resource pack and extract `gfx`, `sfx`, `textures`, and `icons` into:" + ) + print(" - `asset/lang0000/`") + else: + print(f"\n{c.bold(c.green('All assets listed in manifest are valid and present on disk!'))}") + + print() + + +def main(): + parser = argparse.ArgumentParser( + description="Verify asset presence and integrity against Perpetuum resource_0.dat manifest." + ) + parser.add_argument( + "--manifest", + default="asset/resource_0.dat", + help="Path to manifest file (default: asset/resource_0.dat)", + ) + parser.add_argument( + "--asset-dir", + default="asset", + help="Path to asset root directory (default: asset)", + ) + parser.add_argument( + "--layers-dir", + action="append", + default=[], + help="Additional search directories for raw layer .bin files (can be specified multiple times)", + ) + parser.add_argument( + "--check-sizes", + action="store_true", + help="Validate actual file sizes against sizes recorded in manifest", + ) + parser.add_argument( + "--check-definitions", + action="store_true", + help="Cross-reference definitionProperties against asset list", + ) + parser.add_argument( + "--missing-only", + action="store_true", + help="Only display missing or mismatched assets", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output results as JSON", + ) + parser.add_argument( + "--no-color", + action="store_true", + help="Disable ANSI color output", + ) + + args = parser.parse_args() + + colors = Colors(enabled=not args.no_color) + + # Build layer search directories + search_dirs = [ + os.path.join(args.asset_dir, "lang0000/layers/GAMMA_LAYERS_NEW"), + "custom-layers", + "perpetuum-data/layers", + "src/Perpetuum.ServerService2/data/layers", + ] + args.layers_dir + + try: + results = run_checks( + manifest_path=args.manifest, + asset_dir=args.asset_dir, + layer_search_dirs=search_dirs, + check_sizes=args.check_sizes, + check_defs=args.check_definitions, + ) + except Exception as e: + print(colors.red(f"Error: {e}"), file=sys.stderr) + sys.exit(1) + + if args.json: + print(json.dumps(results, indent=2)) + else: + print_report( + results, + colors=colors, + missing_only=args.missing_only, + check_sizes=args.check_sizes, + check_defs=args.check_definitions, + ) + + if results["missing"]: + sys.exit(2) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/script/compose.sh b/script/compose.sh new file mode 100755 index 00000000..978c370f --- /dev/null +++ b/script/compose.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +set -eux + +# Default values +FILE=compose.yml +ENV=.env.local + +docker compose -f $FILE --env-file $ENV "$@" diff --git a/script/migration.sh b/script/migration.sh new file mode 100755 index 00000000..c5bda0a4 --- /dev/null +++ b/script/migration.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env sh + +# Check if /data/done exists (skips on simple container restarts) +if [ -f /data/done ] && [ "$FORCE_MIGRATION" != "true" ]; then + echo "Skipping migration, /data/done already exists." + exit 0 +fi + +set -eux + +CACHE_BAK="/base-data/database/perpetuumsa_migrated.bak" +CACHE_HASH_FILE="/base-data/database/perpetuumsa_migrated.hash" + +# 1. Sync server data and layer files to shared /data +echo "==> Syncing base server data and layers..." +cp -r /perpetuum-service-data/* /data/ +cp -v /work/perpetuum.ini /data/ +cp -r /base-data/layers /data/ +if [ -d /custom-layers ] && [ -n "$(ls -A /custom-layers 2>/dev/null)" ]; then + cp -r /custom-layers/* /data/layers/ +fi + +# Copy all patch-specific data/layers +for d in /migration/Patches/*/Server/data; do + if [ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ]; then + cp -r "$d"/* /data/ + fi +done + +runSqlCmd () { + set +x + sqlcmd -S db -d perpetuumsa -C -U sa -P "${DB_PASSWORD}" -I -i "$1" + set -x +} + +# 2. Compute SHA-256 hash of all migration sources +compute_migration_hash() { + ( + find /migration -type f \( -name "*.sql" -o -name "*.bin" \) -exec sha256sum {} + | sort + [ -f /base-data/database/perpetuumsa.bak ] && sha256sum /base-data/database/perpetuumsa.bak + [ -f /work/restore_DB_to_original_state.sql ] && sha256sum /work/restore_DB_to_original_state.sql + [ -f /work/migration.sh ] && sha256sum /work/migration.sh + ) | sha256sum | awk '{print $1}' +} + +CURRENT_HASH=$(compute_migration_hash) +CACHED_HASH="" +[ -f "$CACHE_HASH_FILE" ] && CACHED_HASH=$(cat "$CACHE_HASH_FILE") + +USE_CACHE=false +if [ "$FORCE_MIGRATION" != "true" ] && [ -f "$CACHE_BAK" ] && [ "$CURRENT_HASH" = "$CACHED_HASH" ]; then + USE_CACHE=true +fi + +# 3. Restore from cache OR run automated full migration +if [ "$USE_CACHE" = "true" ]; then + echo "==> Migration cache HIT (hash matches). Restoring snapshot..." + set +x + sqlcmd -S db -C -U sa -P "${DB_PASSWORD}" -b -I -i "/work/restore_migrated_DB.sql" + set -x + echo "==> Restored migrated DB snapshot in seconds." +else + echo "==> Migration cache MISS or FORCED. Running full migration from scratch..." + + # Create perpetuumsa database if it does not exist + echo "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'perpetuumsa') CREATE DATABASE perpetuumsa" > /work/create-database.sql + sqlcmd -S db -C -U sa -P "${DB_PASSWORD}" -I -i "/work/create-database.sql" + rm /work/create-database.sql + + # Restore base vanilla state + set +x + sqlcmd -S db -C -U sa -P "${DB_PASSWORD}" -b -I -i "/work/restore_DB_to_original_state.sql" + set -x + + # Discover and apply all patches dynamically in chronological version order + PATCH_DIRS=$(ls -1d /migration/Patches/Pre_Alpha_* 2>/dev/null | sort -V; ls -1d /migration/Patches/Live_* 2>/dev/null | sort -V) + + for patch_dir in $PATCH_DIRS; do + [ -d "$patch_dir" ] || continue + patch_name=$(basename "$patch_dir") + echo "==> Applying patch: $patch_name" + + # Check for consolidated patch file + consolidated=$(find "$patch_dir" -maxdepth 1 -type f \( -name "live_patch_*.sql" -o -name "prealpha_patch_*.sql" \) | head -n 1) + + if [ -n "$consolidated" ]; then + runSqlCmd "$consolidated" + elif [ -d "$patch_dir/Raw_SQL" ]; then + # Run all SQL scripts in Raw_SQL in numerical order + find "$patch_dir/Raw_SQL" -maxdepth 1 -type f -name "*.sql" | sort -V | while read -r sql_file; do + runSqlCmd "$sql_file" + done + else + # Run any top-level SQL scripts in the patch folder in order + find "$patch_dir" -maxdepth 1 -type f -name "*.sql" | sort -V | while read -r sql_file; do + runSqlCmd "$sql_file" + done + fi + done + + # Add test account (user: test, pass: test) + runSqlCmd "/migration/Tools/TOOL_test_account.sql" + + echo "==> Creating compressed migration database snapshot..." + set +x + sqlcmd -S db -C -U sa -P "${DB_PASSWORD}" -b -I -Q "BACKUP DATABASE perpetuumsa TO DISK = '/data/perpetuumsa_migrated.bak' WITH FORMAT, INIT, COMPRESSION" + set -x + + # Save hash and set permissions + echo "$CURRENT_HASH" > "$CACHE_HASH_FILE" + chmod 666 "$CACHE_BAK" "$CACHE_HASH_FILE" 2>/dev/null || true + echo "==> Migration snapshot cache saved." +fi + +touch /data/done +echo "Patching complete." diff --git a/script/smoke-test.sh b/script/smoke-test.sh new file mode 100755 index 00000000..e1f8d3c1 --- /dev/null +++ b/script/smoke-test.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# End-to-end smoke test for Perpetuum Server in Docker +# Builds and starts the server stack, asserts on startup logs, +# shuts down gracefully, and asserts on shutdown logs and exit codes. +# +# Exit codes: +# 0 pass +# 2 build failed or container failed to start +# 4 timed out waiting for the server to come online +# 5 a forbidden pattern was found in the log +# 6 the server did not shut down gracefully or reported non-zero exit +# 7 unexpected error + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +TIMEOUT="${TIMEOUT:-180}" +SETTLE_SECONDS="${SETTLE_SECONDS:-15}" +SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-120}" + +COMPOSE_FILE="${REPO_ROOT}/compose.yml" +ENV_FILE="${REPO_ROOT}/.env.local" + +# Helper to run compose commands +compose() { + docker compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@" +} + +REQUIRED_ONLINE="State : \[Online\]" +REQUIRED_OFFLINE="State : \[Off\]" + +FORBIDDEN_PATTERNS=( + "Unhandled exception" + "System\.InvalidOperationException" + "System\.NullReferenceException" + "The current TransactionScope is already complete" + "nesting level exceeded" +) + +write_section() { + echo "" + echo "== $1" +} + +cd "${REPO_ROOT}" + +# --- Phase 0: Environment ------------------------------------------------- +write_section "Environment" +if [ ! -f "${ENV_FILE}" ]; then + echo "Environment file ${ENV_FILE} not found." >&2 + exit 7 +fi +echo "Compose File : ${COMPOSE_FILE}" +echo "Env File : ${ENV_FILE}" +echo "Online Timeout : ${TIMEOUT}s" +echo "Settle Soak : ${SETTLE_SECONDS}s" +echo "Shutdown Timeout : ${SHUTDOWN_TIMEOUT}s" + +# --- Phase 1: Launch ------------------------------------------------------ +write_section "Launch" +echo "Starting stack (db, migration, server)..." +compose stop server 2>/dev/null || true +compose rm -f server 2>/dev/null || true +if ! compose up -d --build --force-recreate server; then + echo "Failed to start server stack." >&2 + exit 2 +fi + +# --- Phase 2: Wait for [Online] ------------------------------------------- +write_section "Waiting for [Online]" +start_time=$(date +%s) +online=0 + +while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + + if [ "$elapsed" -ge "$TIMEOUT" ]; then + break + fi + + if compose logs server 2>&1 | grep -qE "$REQUIRED_ONLINE"; then + online=1 + break + fi + + current_container=$(compose ps -a -q server 2>/dev/null || true) + if [ -n "$current_container" ]; then + status=$(docker inspect -f '{{.State.Status}}' "$current_container" 2>/dev/null || echo "unknown") + if [ "$status" = "exited" ] || [ "$status" = "dead" ]; then + echo "Server container exited unexpectedly with status: $status" >&2 + break + fi + fi + + sleep 1 +done + +elapsed_online=$(( $(date +%s) - start_time )) + +if [ "$online" -ne 1 ]; then + echo "Server did not reach [Online] within ${TIMEOUT} seconds." >&2 + echo "=== Server Logs ===" + compose logs server || true + exit 4 +fi + +echo "Online after ${elapsed_online} seconds." + +# --- Phase 3: Wait for startup to settle ---------------------------------- +write_section "Waiting for startup to settle" +echo "Server is [Online]. Soaking for ${SETTLE_SECONDS}s to allow initial zone flock spawning to complete..." +sleep "$SETTLE_SECONDS" +echo "Startup settle completed after ${SETTLE_SECONDS}s. Proceeding to assertions and shutdown." + +# --- Phase 4: Assert on the startup log ----------------------------------- +write_section "Startup assertions" +startup_log=$(compose logs server 2>&1) +violations=() + +for pattern in "${FORBIDDEN_PATTERNS[@]}"; do + if echo "$startup_log" | grep -qE "$pattern"; then + violations+=("$pattern") + fi +done + +spawn_count=$(echo "$startup_log" | grep -c "member spawned to zone" || true) +flock_count=$(echo "$startup_log" | grep -c "NPCs created" || true) + +echo "Reported values (not asserted):" +printf " %-18s: %s\n" "members spawned" "$spawn_count" +printf " %-18s: %s\n" "flock batches" "$flock_count" +printf " %-18s: %ss\n" "time to online" "$elapsed_online" +printf " %-18s: %ss\n" "settle duration" "$SETTLE_SECONDS" + +# --- Phase 5: Graceful shutdown ------------------------------------------- +write_section "Shutdown" +shutdown_start_time=$(date +%s) + +echo "Stopping server container (sending SIGTERM, timeout ${SHUTDOWN_TIMEOUT}s)..." +if ! compose stop -t "$SHUTDOWN_TIMEOUT" server; then + echo "Failed to stop server container." >&2 + exit 6 +fi + +shutdown_elapsed=$(( $(date +%s) - shutdown_start_time )) +echo "Shutdown took ${shutdown_elapsed}s." + +final_log=$(compose logs server 2>&1) +if ! echo "$final_log" | grep -qE "$REQUIRED_OFFLINE"; then + echo "Server exited but never reported [Off]." >&2 + exit 6 +fi + +server_container=$(compose ps -a -q server 2>/dev/null || true) +if [ -n "$server_container" ]; then + exit_code=$(docker inspect -f '{{.State.ExitCode}}' "$server_container" 2>/dev/null || echo "1") + if [ "$exit_code" -ne 0 ]; then + echo "Server container exit code was $exit_code, expected 0." >&2 + exit 6 + fi +fi + +echo "Graceful shutdown confirmed." + +# --- Phase 6: Verdict ----------------------------------------------------- +write_section "Verdict" +if [ ${#violations[@]} -gt 0 ]; then + echo "Forbidden patterns found in the log:" >&2 + for v in "${violations[@]}"; do + echo " $v" >&2 + done + exit 5 +fi + +echo "SMOKE TEST PASSED" +exit 0 diff --git a/src/Perpetuum.Bootstrapper/Modules/TerrainsModule.cs b/src/Perpetuum.Bootstrapper/Modules/TerrainsModule.cs index 531fb525..28b71a9c 100644 --- a/src/Perpetuum.Bootstrapper/Modules/TerrainsModule.cs +++ b/src/Perpetuum.Bootstrapper/Modules/TerrainsModule.cs @@ -11,6 +11,7 @@ using Perpetuum.Zones.Terrains.Materials.Minerals; using Perpetuum.Zones.Terrains.Materials.Minerals.Generators; using Perpetuum.Zones.Terrains.Materials.Plants; +using SkiaSharp; namespace Perpetuum.Bootstrapper.Modules { @@ -88,7 +89,7 @@ protected override void Load(ContainerBuilder builder) { Terrain terrain = ctx.Resolve(); - System.Drawing.Size size = zone.Configuration.Size; + SKSizeI size = zone.Configuration.Size; ILayerFileIO loader = ctx.Resolve(); diff --git a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs index 89bb3fb5..875ff818 100644 --- a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs +++ b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs @@ -65,7 +65,6 @@ using System.Runtime.Caching; using System.Runtime.Versioning; using System.Text; -using System.Transactions; using IContainer = Autofac.IContainer; using LogEvent = Perpetuum.Log.LogEvent; using Perpetuum.Zones.NpcSystem.Presences.PathFinders; @@ -131,7 +130,7 @@ public void WriteCommandsToFile(string path) File.WriteAllText(path, sb.ToString()); } - public void Init(string gameRoot) + public void Init(string gameRoot, bool distributedTransactions) { _builder = new ContainerBuilder(); InitContainer(gameRoot); @@ -139,6 +138,7 @@ public void Init(string gameRoot) Logger.Current = _container.Resolve>(); GlobalConfiguration config = _container.Resolve(); + config.DistributedTransactions = distributedTransactions; _container.Resolve().State = HostState.Init; // Before anything builds a SqlConnection from it, which happens further down at the @@ -173,7 +173,9 @@ public void Init(string gameRoot) Logger.Info($"GC Latency mode: {GCSettings.LatencyMode}"); Logger.Info($"Vector is hardware accelerated: {Vector.IsHardwareAccelerated}"); - TransactionManager.ImplicitDistributedTransactions = true; + // On platforms without a DTC coordinator (e.g. Linux) enabling this throws + // PlatformNotSupportedException; degrade to local transactions instead of failing to boot. + TransactionSupport.SetImplicitDistributedTransactions(distributedTransactions); Db.DbQueryFactory = _container.Resolve>(); diff --git a/src/Perpetuum.RequestHandlers/Perpetuum.RequestHandlers.csproj b/src/Perpetuum.RequestHandlers/Perpetuum.RequestHandlers.csproj index 47de1543..bca53b08 100644 --- a/src/Perpetuum.RequestHandlers/Perpetuum.RequestHandlers.csproj +++ b/src/Perpetuum.RequestHandlers/Perpetuum.RequestHandlers.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Perpetuum.RequestHandlers/SetRobotTint.cs b/src/Perpetuum.RequestHandlers/SetRobotTint.cs index 6e5046f6..cfe3345a 100644 --- a/src/Perpetuum.RequestHandlers/SetRobotTint.cs +++ b/src/Perpetuum.RequestHandlers/SetRobotTint.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using System.Drawing; using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Robots; +using SkiaSharp; namespace Perpetuum.RequestHandlers { @@ -13,7 +12,7 @@ public void HandleRequest(IRequest request) using (var scope = Db.CreateTransaction()) { var robotEid = request.Data.GetOrDefault(k.robotEID); - var tint = request.Data.GetOrDefault(k.tint); + var tint = request.Data.GetOrDefault(k.tint); var robot = Robot.GetOrThrow(robotEid); robot.Tint = tint; diff --git a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcPlaceSafeSpawnPoint.cs b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcPlaceSafeSpawnPoint.cs index fd4b3a2c..67924c03 100644 --- a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcPlaceSafeSpawnPoint.cs +++ b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcPlaceSafeSpawnPoint.cs @@ -1,6 +1,6 @@ -using System.Drawing; using Perpetuum.Data; using Perpetuum.Host.Requests; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.NpcSafeSpawnPoints { @@ -12,7 +12,7 @@ public override void HandleRequest(IZoneRequest request) { var x = request.Data.GetOrDefault(k.x); var y = request.Data.GetOrDefault(k.y); - AddSafeSpawnPoint(request, new Point(x, y)); + AddSafeSpawnPoint(request, new SKPointI(x, y)); scope.Complete(); } } diff --git a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSafeSpawnPointRequestHandler.cs b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSafeSpawnPointRequestHandler.cs index be954eb0..484b717c 100644 --- a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSafeSpawnPointRequestHandler.cs +++ b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSafeSpawnPointRequestHandler.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using System.Drawing; using System.Transactions; using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Zones.NpcSystem.SafeSpawnPoints; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.NpcSafeSpawnPoints { @@ -36,7 +35,7 @@ void Sender() } } - protected void AddSafeSpawnPoint(IZoneRequest request, Point location) + protected void AddSafeSpawnPoint(IZoneRequest request, SKPointI location) { var point = new SafeSpawnPoint { Location = location }; request.Zone.SafeSpawnPoints.Add(point); diff --git a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSetSafeSpawnPoint.cs b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSetSafeSpawnPoint.cs index 1030e747..386fe1fe 100644 --- a/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSetSafeSpawnPoint.cs +++ b/src/Perpetuum.RequestHandlers/Zone/NpcSafeSpawnPoints/NpcSetSafeSpawnPoint.cs @@ -1,7 +1,7 @@ -using System.Drawing; using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Zones.NpcSystem.SafeSpawnPoints; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.NpcSafeSpawnPoints { @@ -18,7 +18,7 @@ public override void HandleRequest(IZoneRequest request) var point = new SafeSpawnPoint { Id = id, - Location = new Point(x, y) + Location = new SKPointI(x, y) }; request.Zone.SafeSpawnPoints.Update(point); diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DisplaySpots.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DisplaySpots.cs index 37cb22cf..851b6f8a 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DisplaySpots.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DisplaySpots.cs @@ -1,11 +1,11 @@ -using System.Drawing; -using Perpetuum.Services.MissionEngine; +using Perpetuum.Services.MissionEngine; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { public partial class ZoneDrawStatMap { - private Bitmap DisplaySpots() + private SKBitmap DisplaySpots() { var staticObjects = MissionSpot.GetStaticObjectsFromZone(_zone); diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DrawMissionTargetLog.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DrawMissionTargetLog.cs index 55a1440b..bcb7d857 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DrawMissionTargetLog.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/DrawMissionTargetLog.cs @@ -1,17 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Log; using Perpetuum.Services.MissionEngine; using Perpetuum.Services.MissionEngine.MissionStructures; using Perpetuum.Zones; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { @@ -70,7 +64,7 @@ private void DrawMissionTargetLog(IRequest request) internal class MissionTargetSuccessLogEntry { public DateTime EventTime; - public Point point; + public SKPoint point; public MissionTargetType targetType; public Guid guid; public long locationEid; @@ -82,7 +76,7 @@ public static MissionTargetSuccessLogEntry FromRecord(IDataRecord record) var mtsle = new MissionTargetSuccessLogEntry() { EventTime = record.GetValue("eventtime"), - point = new Point(record.GetValue("x"), record.GetValue("y")), + point = new SKPointI(record.GetValue("x"), record.GetValue("y")), targetType = (MissionTargetType) record.GetValue("targettype"), guid = record.GetValue("guid"), locationEid = record.GetValue("locationeid"), @@ -121,8 +115,10 @@ private void DrawOneCategory(IRequest request,MissionLocation missionLocation, M var category1 = category; - bitmap.WithGraphics(gx => gx.DrawString(category1.ToString(), new Font("Tahoma", 15), new SolidBrush(Color.White), new PointF(20, 40))); - bitmap.WithGraphics(gx => gx.DrawString(littleText, new Font("Tahoma", 15), new SolidBrush(Color.White), new PointF(20, 60))); + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 15); + var paint = new SKPaint { Color = SKColors.White }; + bitmap.WithCanvas(gx => gx.DrawText(category1.ToString(), 20, 40 + font.Size, font, paint)); + bitmap.WithCanvas(gx => gx.DrawText(littleText, 20, 60 + font.Size, font, paint)); var idString = $"{missionLocation.id:0000}"; @@ -131,35 +127,33 @@ private void DrawOneCategory(IRequest request,MissionLocation missionLocation, M _saveBitmapHelper.SaveBitmap(_zone,bitmap, fname); } - private void DrawEntriesOnBitmap(MissionTargetSuccessLogEntry[] entries, Bitmap background) + private void DrawEntriesOnBitmap(MissionTargetSuccessLogEntry[] entries, SKBitmap background) { var eventSeries = entries.GroupBy(t => t.guid); - var g = Graphics.FromImage(background); - var pen = new Pen(new SolidBrush(Color.FromArgb(25, Color.FromArgb(200, 200, 200))), 1.1f); - - var switchBrush = new SolidBrush(Color.FromArgb(25, _switchColor)); - var submitItemBrush = new SolidBrush(Color.FromArgb(25, _kioskColor)); - var itemSupplyBrush = new SolidBrush(Color.FromArgb(25, _itemSupplyColor)); - var findArtifactBrush = new SolidBrush(Color.FromArgb(50, _findArtifactColor)); - var popNpcBrush = new SolidBrush(Color.FromArgb(50, _popNpcColor)); - var lootBrush = new SolidBrush(Color.FromArgb(50, _lootColor)); - var fetchItemBrush = new SolidBrush(Color.FromArgb(25, _fetchItemColor)); - var killBrush = new SolidBrush(Color.FromArgb(30, _killColor)); - var scanMineralBrush = new SolidBrush(Color.FromArgb(50, _scanMineralColor)); - var drillMineralBrush = new SolidBrush(Color.FromArgb(50, _drillMineralColor)); - var harvestBrush = new SolidBrush(Color.FromArgb(50, _harvestColor)); + var c = new SKCanvas(background); + var pen = new SKPaint { Color = new SKColor(200, 200, 200, 25), Style = SKPaintStyle.Stroke, StrokeWidth = 1.1f, IsAntialias = true }; + + var switchBrush = new SKPaint { Color = new SKColor(_switchColor.Red, _switchColor.Green, _switchColor.Blue, 25), IsAntialias = true }; + var submitItemBrush = new SKPaint { Color = new SKColor(_kioskColor.Red, _kioskColor.Green, _kioskColor.Blue, 25), Style = SKPaintStyle.Fill, IsAntialias = true }; + var itemSupplyBrush = new SKPaint { Color = new SKColor(_itemSupplyColor.Red, _itemSupplyColor.Green, _itemSupplyColor.Blue, 25), Style = SKPaintStyle.Fill, IsAntialias = true }; + var findArtifactBrush = new SKPaint { Color = new SKColor(_findArtifactColor.Red, _findArtifactColor.Green, _findArtifactColor.Blue, 50), Style = SKPaintStyle.Fill, IsAntialias = true }; + var popNpcBrush = new SKPaint { Color = new SKColor(_popNpcColor.Red, _popNpcColor.Green, _popNpcColor.Blue, 50), Style = SKPaintStyle.Fill, IsAntialias = true }; + var lootBrush = new SKPaint { Color = new SKColor(_lootColor.Red, _lootColor.Green, _lootColor.Blue, 50), Style = SKPaintStyle.Stroke, IsAntialias = true }; + var fetchItemBrush = new SKPaint { Color = new SKColor(_fetchItemColor.Red, _fetchItemColor.Green, _fetchItemColor.Blue, 25), Style = SKPaintStyle.Fill, IsAntialias = true }; + var killBrush = new SKPaint { Color = new SKColor(_killColor.Red, _killColor.Green, _killColor.Blue, 30), Style = SKPaintStyle.Fill, IsAntialias = true }; + var scanMineralBrush = new SKPaint { Color = new SKColor(_scanMineralColor.Red, _scanMineralColor.Green, _scanMineralColor.Blue, 50), Style = SKPaintStyle.Fill, IsAntialias = true }; + var drillMineralBrush = new SKPaint { Color = new SKColor(_drillMineralColor.Red, _drillMineralColor.Green, _drillMineralColor.Blue, 50), Style = SKPaintStyle.Fill, IsAntialias = true }; + var harvestBrush = new SKPaint { Color = new SKColor(_harvestColor.Red, _harvestColor.Green, _harvestColor.Blue, 50), Style = SKPaintStyle.Stroke, IsAntialias = true }; var circle = 10.0f; - g.CompositingQuality = CompositingQuality.HighQuality; - g.SmoothingMode = SmoothingMode.AntiAlias; foreach (var series in eventSeries) { var points = series.OrderBy(v => v.EventTime).Select(v => v.point).ToArray(); - g.DrawLines(pen, points); + c.DrawPoints(SKPointMode.Polygon, points, pen); var eventsAtStructures = series.Where(s => ( s.targetType == MissionTargetType.use_switch || @@ -177,66 +171,76 @@ private void DrawEntriesOnBitmap(MissionTargetSuccessLogEntry[] entries, Bitmap foreach (var logEntry in eventsAtStructures) { - Brush p; - Pen pp; + SKPaint paint; switch (logEntry.targetType) { case MissionTargetType.submit_item: - p = submitItemBrush; - g.FillEllipse(p, logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + paint = submitItemBrush; + SKRect rect = new(logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + c.DrawOval(rect, paint); continue; case MissionTargetType.use_switch: - p = switchBrush; - g.FillEllipse(p, logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + paint = switchBrush; + rect = new(logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + c.DrawOval(rect, paint); continue; case MissionTargetType.use_itemsupply: - p = itemSupplyBrush; - g.FillEllipse(p, logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + paint = itemSupplyBrush; + rect = new(logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + c.DrawOval(rect, paint); continue; case MissionTargetType.find_artifact: - p = findArtifactBrush; - g.FillRectangle(p, logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + paint = findArtifactBrush; + rect = new(logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + c.DrawOval(rect, paint); continue; case MissionTargetType.pop_npc: - p = popNpcBrush; - g.FillRectangle(p, logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + paint = popNpcBrush; + rect = new(logEntry.point.X - circle / 2.0f, logEntry.point.Y - circle / 2.0f, circle, circle); + c.DrawOval(rect, paint); continue; case MissionTargetType.loot_item: - pp = new Pen(lootBrush, 3); + paint = lootBrush; const int lootSize = 11; - g.DrawRectangle(pp, logEntry.point.X - lootSize / 2.0f, logEntry.point.Y - lootSize / 2.0f, lootSize, lootSize); + rect = new(logEntry.point.X - lootSize / 2.0f, logEntry.point.Y - lootSize / 2.0f, lootSize, lootSize); + c.DrawOval(rect, paint); continue; case MissionTargetType.fetch_item: - pp = new Pen(fetchItemBrush, 4); + paint = fetchItemBrush; const int fetchSize = 14; - g.DrawRectangle(pp, logEntry.point.X - fetchSize / 2.0f, logEntry.point.Y - fetchSize / 2.0f, fetchSize, fetchSize); + rect = new(logEntry.point.X - fetchSize / 2.0f, logEntry.point.Y - fetchSize / 2.0f, fetchSize, fetchSize); + c.DrawOval(rect, paint); continue; case MissionTargetType.kill_definition: const int tizenKetto = 12; - pp = new Pen(killBrush, 4); - g.DrawRectangle(pp, logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + rect = new(logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + paint = killBrush; + c.DrawRect(rect, paint); continue; case MissionTargetType.scan_mineral: - pp = new Pen(scanMineralBrush, 4); - g.DrawEllipse(pp, logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + paint = scanMineralBrush; + rect = new(logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + c.DrawOval(rect, paint); continue; case MissionTargetType.drill_mineral: - pp = new Pen(drillMineralBrush, 4); - g.DrawEllipse(pp, logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + paint = drillMineralBrush; + rect = new(logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + c.DrawOval(rect, paint); continue; case MissionTargetType.harvest_plant: - pp = new Pen(harvestBrush, 4); - g.DrawEllipse(pp, logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + paint = harvestBrush; + rect = new(logEntry.point.X - tizenKetto / 2.0f, logEntry.point.Y - tizenKetto / 2.0f, tizenKetto, tizenKetto); + c.DrawOval(rect, paint ); continue; default: @@ -254,7 +258,7 @@ private void DrawEntriesOnBitmap(MissionTargetSuccessLogEntry[] entries, Bitmap - private Bitmap DrawAllTargetsOnZone() + private SKBitmap DrawAllTargetsOnZone() { const string query = "SELECT * FROM dbo.missiontargetslog WHERE zoneid=@zoneId"; diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateMissionSpots.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateMissionSpots.cs index cd380e5d..bbde225f 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateMissionSpots.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateMissionSpots.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Linq; -using System.Threading.Tasks; -using Perpetuum.Data; +using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Log; using Perpetuum.Services.MissionEngine; using Perpetuum.Zones; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { @@ -143,7 +138,7 @@ private class AccuracyInfo private const int structureToTerminals = 50; private const int randomPointToTerminals = 70; - private Bitmap GenerateMissionSpots(IRequest request) + private SKBitmap GenerateMissionSpots(IRequest request) { //-------- kick brute force fill in @@ -176,26 +171,26 @@ private Bitmap GenerateMissionSpots(IRequest request) return resultBitmap; } - private static readonly Color _passableColor = Color.FromArgb(255,16, 26, 26); - private static readonly Color _fieldTerminalColor = Color.White; - private static readonly Color _switchColor = Color.FromArgb(255,255, 82, 0); - private static readonly Color _kioskColor = Color.FromArgb(255,153, 206, 70); - private static readonly Color _itemSupplyColor = Color.FromArgb(255,48, 198, 249); - private static readonly Color _randomPointColor = Color.FromArgb(255,237, 144, 251); - private static readonly Color _dockingBaseColor = Color.FromArgb(255,21, 68, 29); - private static readonly Color _teleportColor = Color.FromArgb(255,74, 78, 6); - private static readonly Color _sapColor = Color.FromArgb(255,54, 29, 99); - private static readonly Color _islandColor = Color.FromArgb(255,0, 24, 59); - private static readonly Color _findArtifactColor = Color.FromArgb(255,255, 204, 77); - private static readonly Color _popNpcColor = Color.FromArgb(255, 105, 82, 0); - private static readonly Color _lootColor = Color.FromArgb(255, 0, 151, 208); - private static readonly Color _fetchItemColor = Color.FromArgb(255, 12, 137, 119); - private static readonly Color _killColor = Color.FromArgb(255, 152, 15, 15); - private static readonly Color _scanMineralColor = Color.FromArgb(255, 124, 164, 255); - private static readonly Color _drillMineralColor = Color.FromArgb(255, 214, 144, 126); - private static readonly Color _harvestColor = Color.FromArgb(255, 164, 231, 72); - - private Bitmap DrawResultOnBitmap(List spotInfos, Dictionary> staticObjects ) + private static readonly SKColor _passableColor = new(16, 26, 26); + private static readonly SKColor _fieldTerminalColor = SKColors.White; + private static readonly SKColor _switchColor = new(255, 82, 0); + private static readonly SKColor _kioskColor = new(153, 206, 70); + private static readonly SKColor _itemSupplyColor = new(48, 198, 249); + private static readonly SKColor _randomPointColor = new(237, 144, 251); + private static readonly SKColor _dockingBaseColor = new(21, 68, 29); + private static readonly SKColor _teleportColor = new(74, 78, 6); + private static readonly SKColor _sapColor = new(54, 29, 99); + private static readonly SKColor _islandColor = new(0, 24, 59); + private static readonly SKColor _findArtifactColor = new(255, 204, 77); + private static readonly SKColor _popNpcColor = new(105, 82, 0); + private static readonly SKColor _lootColor = new(0, 151, 208); + private static readonly SKColor _fetchItemColor = new(12, 137, 119); + private static readonly SKColor _killColor = new(152, 15, 15); + private static readonly SKColor _scanMineralColor = new(124, 164, 255); + private static readonly SKColor _drillMineralColor = new(214, 144, 126); + private static readonly SKColor _harvestColor = new(164, 231, 72); + + private SKBitmap DrawResultOnBitmap(List spotInfos, Dictionary> staticObjects ) { var b = _zone.CreatePassableBitmap(_passableColor); @@ -227,23 +222,23 @@ private Bitmap DrawResultOnBitmap(List spotInfos, Dictionary spotInfos, Dictionary g.DrawString(fttext, new Font("Tahoma", 15), new SolidBrush(_fieldTerminalColor), new PointF(20, 40))); - b.WithGraphics(g => g.DrawString(swtext, new Font("Tahoma", 15), new SolidBrush(_switchColor), new PointF(20, 60))); - b.WithGraphics(g => g.DrawString(kiotext, new Font("Tahoma", 15), new SolidBrush(_kioskColor), new PointF(20, 80))); - b.WithGraphics(g => g.DrawString(istext, new Font("Tahoma", 15), new SolidBrush(_itemSupplyColor), new PointF(20, 100))); - b.WithGraphics(g => g.DrawString(rptext, new Font("Tahoma", 15), new SolidBrush(_randomPointColor), new PointF(20, 120))); + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 15); + var fieldTerminalColorPaint = new SKPaint { Color = _fieldTerminalColor }; + var switchPaint = new SKPaint { Color = _switchColor }; + var kioskPaint = new SKPaint { Color = _kioskColor }; + var itemSupplyPaint = new SKPaint { Color = _itemSupplyColor }; + var randomPointPaint = new SKPaint { Color = _randomPointColor }; + + b.WithCanvas(c => c.DrawText(fttext, 20, 40 + font.Size, font, fieldTerminalColorPaint)); + b.WithCanvas(c => c.DrawText(swtext, 20, 60 + font.Size, font, switchPaint)); + b.WithCanvas(c => c.DrawText(kiotext, 20, 80 + font.Size, font, kioskPaint)); + b.WithCanvas(c => c.DrawText(istext, 20, 100 + font.Size, font, itemSupplyPaint)); + b.WithCanvas(c => c.DrawText(rptext, 20, 120 + font.Size, font, randomPointPaint)); return b; } - private void FillEllipseOnPoint(Color color, int radius, Position position, Bitmap bitmap) + private void FillEllipseOnPoint(SKColor color, int radius, Position position, SKBitmap bitmap) { - var gfx = Graphics.FromImage(bitmap); - gfx.CompositingQuality = CompositingQuality.HighQuality; - gfx.SmoothingMode = SmoothingMode.AntiAlias; + var c = new SKCanvas(bitmap); var size = radius * 2; var x = position.intX - radius; var y = position.intY - radius; - gfx.FillEllipse(new SolidBrush(color),x,y,size,size ); + var paint = new SKPaint { Color = color, Style = SKPaintStyle.Fill, IsAntialias = true }; + var rect = new SKRect(x,y,size,size); + c.DrawOval(rect, paint); /* @@ -306,18 +308,17 @@ private void FillEllipseOnPoint(Color color, int radius, Position position, Bitm } - private void DrawEllipseOnPoint(Color color, int radius, Position position, Bitmap bitmap) + private void DrawEllipseOnPoint(SKColor color, int radius, Position position, SKBitmap bitmap) { - var gfx = Graphics.FromImage(bitmap); - gfx.CompositingQuality = CompositingQuality.HighQuality; - gfx.SmoothingMode = SmoothingMode.AntiAlias; + var c = new SKCanvas(bitmap); var size = radius * 2; var x = position.intX - radius; var y = position.intY - radius; - gfx.DrawEllipse( new Pen(color,3), x, y, size, size); - + var paint = new SKPaint { Color = color, Style = SKPaintStyle.Stroke, StrokeWidth = 3, IsAntialias = true }; + var rect = new SKRect(x, y, size, size); + c.DrawOval(rect, paint); } @@ -335,7 +336,7 @@ private void PlaceOneType(List spotInfos, MissionSpotType type, int var currentBorder = accuracyInfo.initialBorder; var foundTotal = 0; - var freePoints = new List(_zone.Configuration.Size.Width * _zone.Configuration.Size.Height); + var freePoints = new List(_zone.Configuration.Size.Width * _zone.Configuration.Size.Height); InitPoints(spotInfos, distanceInfos, staticObjects, freePoints); while (true) @@ -421,7 +422,7 @@ private static void SaveInfoAsync(MissionSpot si) Task.Run(() => { si.Save(); }); } - private void InitPoints(List spotInfos, Dictionary distanceInfos, Dictionary> staticObjects, List freePoints) + private void InitPoints(List spotInfos, Dictionary distanceInfos, Dictionary> staticObjects, List freePoints) { var zoneWidth = _zone.Size.Width; var zoneHeight = _zone.Size.Height; @@ -437,16 +438,16 @@ private void InitPoints(List spotInfos, Dictionary freePoints) + private static void CleanUpOneSpot(Position center, int distance, ref List freePoints) { - var goodKeys = new List(freePoints.Count); + var goodKeys = new List(freePoints.Count); foreach (var point in freePoints) { var pos = point.ToPosition(); @@ -583,9 +584,9 @@ private bool CheckConditionsAroundPosition(Position center, int blockRadius, int private int _counter; - private void MakeASnapshot(MissionSpotType spotType, List freePoints) + private void MakeASnapshot(MissionSpotType spotType, List freePoints) { - var pointsCopy = new List(freePoints); + var pointsCopy = new List(freePoints); _counter++; var fileName = spotType + "_freepoints." + $"{_counter:0000}"; @@ -596,7 +597,7 @@ private void MakeASnapshot(MissionSpotType spotType, List freePoints) foreach (var point in pointsCopy) { - bmp.SetPixel(point.X, point.Y, Color.White); + bmp.SetPixel(point.X, point.Y, SKColors.White); } _saveBitmapHelper.SaveBitmap(_zone,bmp, fileName); diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateRandomPointsOnly.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateRandomPointsOnly.cs index d49d6a00..7e4f9113 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateRandomPointsOnly.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/GenerateRandomPointsOnly.cs @@ -1,17 +1,16 @@ -using System.Drawing; -using System.Linq; -using Perpetuum.Data; +using Perpetuum.Data; using Perpetuum.Host.Requests; using Perpetuum.Log; using Perpetuum.Services.MissionEngine; using Perpetuum.Zones; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { public partial class ZoneDrawStatMap { - private Bitmap GenerateRandomPointsOnly(IRequest request) + private SKBitmap GenerateRandomPointsOnly(IRequest request) { //-------- kick brute force fill in const int randomPointTargetAmount = 2500; diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/ValidateMissionObjectLocations.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/ValidateMissionObjectLocations.cs index 07573987..facbd435 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/ValidateMissionObjectLocations.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/ValidateMissionObjectLocations.cs @@ -1,11 +1,10 @@ -using System.Drawing; -using System.Linq; -using Perpetuum.Log; +using Perpetuum.Log; using Perpetuum.Services.MissionEngine; using Perpetuum.Services.MissionEngine.MissionStructures; using Perpetuum.Units.DockingBases; using Perpetuum.Units.FieldTerminals; using Perpetuum.Zones; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { @@ -13,18 +12,18 @@ public partial class ZoneDrawStatMap { - public Bitmap ValidateMissionObjectLocations() + public SKBitmap ValidateMissionObjectLocations() { var b = _zone.CreatePassableBitmap(_passableColor); - var g = Graphics.FromImage(b); + var c = new SKCanvas(b); var circle = 10f; var randomPointTargets = _missionDataCache.GetAllMissionTargets.Where(t => t.ZoneId == _zone.Id && t.Type == MissionTargetType.rnd_point).ToList(); - var greebrush = new SolidBrush(Color.LawnGreen); - var redBrush = new SolidBrush(Color.OrangeRed); - var yellowBrush = new SolidBrush(Color.Yellow); - var redPen = new Pen(Color.Red, 4); + var greenbrush = new SKPaint { Color = SKColors.LawnGreen, Style = SKPaintStyle.Fill }; + var redBrush = new SKPaint { Color = SKColors.OrangeRed, Style = SKPaintStyle.Fill }; + var yellowBrush = new SKPaint { Color = SKColors.Yellow, Style = SKPaintStyle.Fill }; + var redPen = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Stroke, StrokeWidth = 4 }; foreach (var randomPointTarget in randomPointTargets) { @@ -32,11 +31,13 @@ public Bitmap ValidateMissionObjectLocations() if (CheckConditionsAroundPosition(p, randomPointBlockRadius, randomPointIslandRadius, true)) { - g.FillEllipse(greebrush,(float)( randomPointTarget.targetPosition.X - circle ), (float)( randomPointTarget.targetPosition.Y - circle ), circle*2, circle*2); + var rect = new SKRect((float)(randomPointTarget.targetPosition.X - circle), (float)(randomPointTarget.targetPosition.Y - circle), circle * 2, circle * 2); + c.DrawOval(rect, greenbrush); } else { - g.FillEllipse(redBrush, (float)(randomPointTarget.targetPosition.X - circle ), (float)(randomPointTarget.targetPosition.Y - circle ), circle*2, circle*2); + var rect = new SKRect((float)(randomPointTarget.targetPosition.X - circle ), (float)(randomPointTarget.targetPosition.Y - circle ), circle*2, circle*2); + c.DrawOval(rect, redBrush); } } @@ -50,7 +51,8 @@ public Bitmap ValidateMissionObjectLocations() if (strucureTarget == null) { Logger.Error("no target was found for structure:" + structureUnit.Eid + " " + structureUnit.TargetType); - g.FillEllipse(yellowBrush, structureUnit.CurrentPosition.intX - circle, structureUnit.CurrentPosition.intY - circle, circle*2, circle*2); + var rect = new SKRect(structureUnit.CurrentPosition.intX - circle, structureUnit.CurrentPosition.intY - circle, circle*2, circle*2); + c.DrawOval(rect, yellowBrush); continue; } @@ -68,7 +70,8 @@ public Bitmap ValidateMissionObjectLocations() if (location == null) { - g.DrawEllipse(redPen, locationUnit.CurrentPosition.intX - circle, locationUnit.CurrentPosition.intY - circle, circle * 2, circle * 2); + var rect = new SKRect(locationUnit.CurrentPosition.intX - circle, locationUnit.CurrentPosition.intY - circle, circle * 2, circle * 2); + c.DrawOval(rect, redPen); Logger.Error("no location was found for " + locationUnit); continue; } diff --git a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/WorstMissionSpots.cs b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/WorstMissionSpots.cs index 8f1b0635..9a86feea 100644 --- a/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/WorstMissionSpots.cs +++ b/src/Perpetuum.RequestHandlers/Zone/StatsMapDrawing/WorstMissionSpots.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using Perpetuum.Log; +using Perpetuum.Log; using Perpetuum.Services.MissionEngine; using Perpetuum.Zones; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone.StatsMapDrawing { public partial class ZoneDrawStatMap { - private Bitmap DrawWorstSpotsMap() + private SKBitmap DrawWorstSpotsMap() { _addtoradius = 0; //init the mf var b = _zone.CreatePassableBitmap(_passableColor, _islandColor); @@ -35,11 +32,15 @@ private Bitmap DrawWorstSpotsMap() WriteReportByType(MissionSpotType.kiosk, spotStats,b); WriteReportByType(MissionSpotType.itemsupply, spotStats,b); - - b.WithGraphics(g => g.DrawString("switch", new Font("Tahoma", 15), new SolidBrush(_switchColor), new PointF(20, 60))); - b.WithGraphics(g => g.DrawString("item submit/kiosk", new Font("Tahoma", 15), new SolidBrush(_kioskColor), new PointF(20, 80))); - b.WithGraphics(g => g.DrawString("item supply", new Font("Tahoma", 15), new SolidBrush(_itemSupplyColor), new PointF(20, 100))); - b.WithGraphics(g => g.DrawString("random point", new Font("Tahoma", 15), new SolidBrush(_randomPointColor), new PointF(20, 120))); + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 15); + var switchPaint = new SKPaint { Color = _switchColor }; + var kioskPaint = new SKPaint { Color = _kioskColor }; + var itemSupplyPaint = new SKPaint { Color = _itemSupplyColor }; + var randomPointPaint = new SKPaint { Color = _randomPointColor }; + b.WithCanvas(c => c.DrawText("switch", 20, 60 + font.Size, font, switchPaint)); + b.WithCanvas(c => c.DrawText("item submit/kiosk", 20, 80 + font.Size, font, kioskPaint)); + b.WithCanvas(c => c.DrawText("item supply", 20, 100 + font.Size, font, itemSupplyPaint)); + b.WithCanvas(c => c.DrawText("random point", 20, 120 + font.Size, font, randomPointPaint)); return b; @@ -47,7 +48,7 @@ private Bitmap DrawWorstSpotsMap() } private int _addtoradius; - private void WriteReportByType(MissionSpotType missionSpotType, List spotStats, Bitmap bitmap) + private void WriteReportByType(MissionSpotType missionSpotType, List spotStats, SKBitmap bitmap) { var lines = new List(spotStats.Count); var ordered = spotStats.OrderBy(s => s.GetAmountByType(missionSpotType)).ToArray(); @@ -72,7 +73,7 @@ private void WriteReportByType(MissionSpotType missionSpotType, List _zone.CreatePassableBitmap(Color.White)); + RegisterCreator("passable", () => _zone.CreatePassableBitmap(SKColors.White)); RegisterCreator("islandmask", CreateIslandMaskMap); RegisterCreator("controlmap", CreateControlMap); RegisterCreator("TerraformProtected", CreateControlFlagMap(TerrainControlFlags.TerraformProtected)); @@ -74,31 +73,33 @@ public ZoneDrawStatMap(IFileSystem fileSystem, SaveBitmapHelper saveBitmapHelper RegisterCreator(k.groundType, CreateGroundTypeMap); } - private void RegisterCreator(string type, Func bitmapFactory) + private void RegisterCreator(string type, Func bitmapFactory) { _actions[type] = (r) => CreateAndSave(type, () => bitmapFactory(r)); } - private void RegisterCreator(string type, Func bitmapFactory) + private void RegisterCreator(string type, Func bitmapFactory) { _actions[type] = (r) => CreateAndSave(type, bitmapFactory); } - private void CreateAndSave(string postfix, Func bitmapFactory) + private void CreateAndSave(string postfix, Func bitmapFactory) { - Bitmap bmp = bitmapFactory(); + SKBitmap bmp = bitmapFactory(); if (bmp == null) { return; } - bmp.WithGraphics(g => g.DrawString(_zone.Configuration.Name, new Font("Tahoma", 20), Brushes.Red, new PointF(10, 10))); + var paint = new SKPaint { Color = SKColors.Red }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 20); + bmp.WithCanvas(c => c.DrawText(_zone.Configuration.Name, 10, 10 + font.Size, font, paint)); string fileName = "stat_" + postfix; if (_sendtoclient) // send to client. { using MemoryStream ms = new(); - bmp.Save(ms, ImageFormat.Png); + bmp.Encode(ms, SKEncodedImageFormat.Png, 100); string Base64 = Convert.ToBase64String(ms.GetBuffer()); Message.Builder.FromRequest(_request).SetData("name", fileName).SetData("img", Base64).Send(); } @@ -251,17 +252,17 @@ public void HandleRequest(IZoneRequest request) Message.Builder.FromRequest(request).WithData(data).Send(); } - private Bitmap CreateAltitudeBitmap() + private SKBitmap CreateAltitudeBitmap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { byte altitudeValue = (byte)(_zone.Terrain.Altitude.GetAltitudeAsDouble(x, y) / 2048 * 612).Clamp(0, 255); - Color color = Color.FromArgb(altitudeValue, altitudeValue, altitudeValue); + SKColor color = new(altitudeValue, altitudeValue, altitudeValue); bmp.SetPixel(x, y, color); }); } - private Bitmap CreateSlopeBitmap() + private SKBitmap CreateSlopeBitmap() { const int threshold = 4 * 4; @@ -275,12 +276,12 @@ private Bitmap CreateSlopeBitmap() return; } - int c = 255 - (int)((double)slope / threshold * 255); - bmp.SetPixel(x, y, Color.FromArgb(c, c, c)); + byte c = (byte)(255 - (int)((double)slope / threshold * 255)); + bmp.SetPixel(x, y, new(c, c, c)); }); } - private Bitmap CreateBlockingMap() + private SKBitmap CreateBlockingMap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { @@ -290,7 +291,7 @@ private Bitmap CreateBlockingMap() return; } - bmp.SetPixel(x, y, Color.White); + bmp.SetPixel(x, y, SKColors.White); }); } @@ -300,15 +301,15 @@ private void GenerateNewFlagsMap() } - private Bitmap CreateNewFlagsMap() + private SKBitmap CreateNewFlagsMap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { TerrainControlInfo ci = _zone.Terrain.Controls.GetValue(x, y); - int r = 0; - int g = 0; - int b = 0; + byte r = 0; + byte g = 0; + byte b = 0; if (ci.PBSHighway) { @@ -325,51 +326,59 @@ private Bitmap CreateNewFlagsMap() b = 255; } - Color color = Color.FromArgb(255, r, g, b); + SKColor color = new(r, g, b); bmp.SetPixel(x, y, color); }); } - private Bitmap CreatePlayersMap() + private SKBitmap CreatePlayersMap() { - return CreateAltitudeBitmap().WithGraphics(g => + return CreateAltitudeBitmap().WithCanvas(c => { foreach (Accounting.Characters.Character unit in _zone.GetCharacters()) { int size = 12; - Pen pen = Pens.Red; int x = unit.GetPlayerRobotFromZone().CurrentPosition.intX - (size / 2); int y = unit.GetPlayerRobotFromZone().CurrentPosition.intY - (size / 2); - g.DrawEllipse(pen, x, y, size, size); + var pen = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Stroke }; + var rect1 = new SKRectI(x, y, size, size); + c.DrawOval(rect1, pen); const int width = 4; - g.DrawEllipse(Pens.BlueViolet, x, y, width, width); - g.DrawString(unit.Nick, new Font("Tahoma", 12), Brushes.Red, x + 10, y + 10); + var pen2 = new SKPaint { Color = SKColors.BlueViolet, Style = SKPaintStyle.Stroke }; + var rect2 = new SKRectI(x, y, width, width); + c.DrawOval(rect2, pen2); + var textPaint = new SKPaint { Color = SKColors.Red }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 12); + c.DrawText(unit.Nick, x + 10, y + 10 + font.Size, font, textPaint); } }); } - private Bitmap CreateNPCMap() + private SKBitmap CreateNPCMap() { - return CreateAltitudeBitmap().WithGraphics(g => + return CreateAltitudeBitmap().WithCanvas(g => { DrawNpcPresencesOnGraphic(g); DrawNpcFlocksOnGraphic(g); }); } - private void DrawNpcPresencesOnGraphic(Graphics graphics) + private void DrawNpcPresencesOnGraphic(SKCanvas canvas) { foreach (RoamingPresence presence in _zone.PresenceManager.GetPresences().OfType()) { - graphics.DrawRectangle(Pens.Blue, presence.Area.X1, presence.Area.Y1, presence.Area.Width, presence.Area.Height); - graphics.DrawString(presence.Configuration.Name, new Font("Tahoma", 8), Brushes.Red, presence.Area.X1, presence.Area.Y1); + var paint = new SKPaint { Color = SKColors.Blue, Style = SKPaintStyle.Stroke }; + canvas.DrawRect(presence.Area.X1, presence.Area.Y1, presence.Area.Width, presence.Area.Height, paint); + var textPaint = new SKPaint { Color = SKColors.Red }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 8); + canvas.DrawText(presence.Configuration.Name, presence.Area.X1, presence.Area.Y1 + font.Size, font, textPaint); } } - private void DrawNpcFlocksOnGraphic(Graphics graphics) + private void DrawNpcFlocksOnGraphic(SKCanvas canvas) { foreach (Zones.NpcSystem.Flocks.Flock? flock in _zone.PresenceManager.GetPresences().OfType().SelectMany(p => p.Flocks)) { @@ -381,18 +390,24 @@ private void DrawNpcFlocksOnGraphic(Graphics graphics) int tyHomeRange = flock.Configuration.SpawnOrigin.intY - flock.HomeRange; int widthHome = flock.HomeRange * 2; - graphics.DrawEllipse(Pens.BlueViolet, txSpawnMax, tySpawnMax, widthSpawnMax, widthSpawnMax); - graphics.DrawEllipse(Pens.Red, txHomeRange, tyHomeRange, widthHome, widthHome); - graphics.DrawString(flock.Configuration.Name, new Font("Tahoma", 10), Brushes.Red, txSpawnMax, tySpawnMax); + var pen1 = new SKPaint { Color = SKColors.BlueViolet, Style = SKPaintStyle.Stroke }; + var rect1 = new SKRectI(txSpawnMax, tySpawnMax, widthSpawnMax, widthSpawnMax); + canvas.DrawOval(rect1, pen1); + var pen2 = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Stroke }; + var rect2 = new SKRectI(txHomeRange, tyHomeRange, widthHome, widthHome); + canvas.DrawOval(rect2, pen2); + var textPaint = new SKPaint { Color = SKColors.Red }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 10); + canvas.DrawText(flock.Configuration.Name, txSpawnMax, tySpawnMax + font.Size, font, textPaint); } } - private Bitmap CreateMissionTargetsMap() + private SKBitmap CreateMissionTargetsMap() { - return CreateAltitudeBitmap().WithGraphics(DrawMissionTargetsOnGraphics); + return CreateAltitudeBitmap().WithCanvas(DrawMissionTargetsOnGraphics); } - private void DrawMissionTargetsOnGraphics(Graphics graphics) + private void DrawMissionTargetsOnGraphics(SKCanvas canvas) { List targets = _missionDataCache.GetAllMissionTargets.Where(t => t.ValidZoneSet && t.ZoneId == _zone.Id).ToList(); @@ -411,14 +426,18 @@ private void DrawMissionTargetsOnGraphics(Graphics graphics) int tx = missionTarget.targetPosition.intX - 2; int ty = missionTarget.targetPosition.intY - 2; const int width = 4; - graphics.DrawEllipse(Pens.BlueViolet, tx, ty, width, width); - graphics.DrawString(targetNames[missionTarget.id], new Font("Tahoma", 8), Brushes.White, tx, ty); + var pen = new SKPaint{ Color = SKColors.BlueViolet, Style = SKPaintStyle.Stroke }; + var rect = new SKRectI(tx, ty, width, width); + canvas.DrawOval(rect, pen); + var textPaint = new SKPaint { Color = SKColors.White }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 8); + canvas.DrawText(targetNames[missionTarget.id], tx, ty + font.Size, font, textPaint); } } - private Bitmap CreateDecorBlockingMap() + private SKBitmap CreateDecorBlockingMap() { return CreateAltitudeBitmap().ForEach((bmp, x, y) => { @@ -428,12 +447,12 @@ private Bitmap CreateDecorBlockingMap() return; } - Color color = blockingInfo.Height > 0 ? Color.Green : Color.Orange; + SKColor color = blockingInfo.Height > 0 ? SKColors.Green : SKColors.Orange; bmp.SetPixel(x, y, color); }); } - private Bitmap CreateElectroPlantMap() + private SKBitmap CreateElectroPlantMap() { return CreateAltitudeBitmap().ForEach((bmp, x, y) => { @@ -444,12 +463,12 @@ private Bitmap CreateElectroPlantMap() return; } - int r = (255 / 5 * plantInfo.state).Clamp(0, 255); - Color color = Color.FromArgb(255, r, 128, 0); + byte r = (byte)(255 / 5 * plantInfo.state).Clamp(0, 255); + SKColor color = new(r, 128, 0); if (plantInfo.state == 0) { - color = Color.FromArgb(255, 255, 0, 30); + color = new(255, 0, 30); } bmp.SetPixel(x, y, color); @@ -457,7 +476,7 @@ private Bitmap CreateElectroPlantMap() } - private Bitmap CreatePlantMap(PlantType plantType) + private SKBitmap CreatePlantMap(PlantType plantType) { return CreateAltitudeBitmap().ForEach((bmp, x, y) => { @@ -468,12 +487,12 @@ private Bitmap CreatePlantMap(PlantType plantType) return; } - int r = (255 / 5 * plantInfo.state).Clamp(0, 255); - Color color = Color.FromArgb(255, r, 128, 0); + byte r = (byte)(255 / 5 * plantInfo.state).Clamp(0, 255); + SKColor color = new(r, 128, 0); if (plantInfo.state == 0) { - color = Color.FromArgb(255, 255, 0, 30); + color = new(255, 0, 30); } bmp.SetPixel(x, y, color); @@ -481,7 +500,7 @@ private Bitmap CreatePlantMap(PlantType plantType) } - private Bitmap CreatePlantsMap() + private SKBitmap CreatePlantsMap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { @@ -497,44 +516,45 @@ private Bitmap CreatePlantsMap() return; } - bmp.SetPixel(x, y, Color.White); + bmp.SetPixel(x, y, SKColors.White); }); } - private Bitmap CreateStructuresMap() + private SKBitmap CreateStructuresMap() { - return _zone.CreateBitmap().WithGraphics(g => + return _zone.CreateBitmap().WithCanvas(c => { foreach (Unit unit in _zone.GetStaticUnits()) { int size = 3; - Pen pen = Pens.White; + var pen = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Stroke }; if (unit.IsCategory(CategoryFlags.cf_outpost)) { - pen = Pens.LightSeaGreen; + pen.Color = SKColors.LightSeaGreen; size = 150; } else if (unit.IsCategory(CategoryFlags.cf_public_docking_base)) { - pen = Pens.Yellow; + pen.Color = SKColors.Yellow; size = 150; } else if (unit.IsCategory(CategoryFlags.cf_teleport_column)) { - pen = Pens.WhiteSmoke; + pen.Color = SKColors.WhiteSmoke; size = 100; } int x = unit.CurrentPosition.intX - (size / 2); int y = unit.CurrentPosition.intY - (size / 2); - g.DrawEllipse(pen, x, y, size, size); + var rect = new SKRectI(x, y, size, size); + c.DrawOval(rect, pen); } }); } - private Bitmap CreateWallMap() + private SKBitmap CreateWallMap() { return CreateAltitudeBitmap().ForEach((bmp, x, y) => { @@ -544,14 +564,14 @@ private Bitmap CreateWallMap() return; } - int r = (255 / 11 * pInfo.state).Clamp(0, 255); + byte r = (byte)(255 / 11 * pInfo.state).Clamp(0, 255); byte g = pInfo.health; - Color pColor = Color.FromArgb(255, r, g, 0); + SKColor pColor = new(r, g, 0); bmp.SetPixel(x, y, pColor); }); } - private Bitmap CreateWallPossibleMap() + private SKBitmap CreateWallPossibleMap() { Outpost[] outposts = _zone.Units.OfType().ToArray(); Teleport[] teleports = _zone.Units.OfType().ToArray(); @@ -587,11 +607,11 @@ private Bitmap CreateWallPossibleMap() return; } - Color pixel = bmp.GetPixel(x, y); + SKColor pixel = bmp.GetPixel(x, y); - byte r = pixel.R; - byte g = pixel.G; - byte b = pixel.B; + byte r = pixel.Red; + byte g = pixel.Green; + byte b = pixel.Blue; if (allowed) { @@ -605,12 +625,12 @@ private Bitmap CreateWallPossibleMap() b = 0; } - bmp.SetPixel(x, y, Color.FromArgb(255, r, g, b)); + bmp.SetPixel(x, y, new(r, g, b)); }); } - private Bitmap CreateWallPlaces() + private SKBitmap CreateWallPlaces() { Outpost[] outposts = _zone.Units.OfType().ToArray(); Teleport[] teleports = _zone.Units.OfType().ToArray(); @@ -640,12 +660,12 @@ private Bitmap CreateWallPlaces() if (allowed) { - bmp.SetPixel(x, y, Color.FromArgb(255, 255, 0, 0)); + bmp.SetPixel(x, y, new(255, 0, 0)); } }); } - private Bitmap CreateIslandMaskMap() + private SKBitmap CreateIslandMaskMap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { @@ -655,11 +675,11 @@ private Bitmap CreateIslandMaskMap() return; } - bmp.SetPixel(x, y, Color.White); + bmp.SetPixel(x, y, SKColors.White); }); } - private Func CreateControlFlagMap(TerrainControlFlags flag) + private Func CreateControlFlagMap(TerrainControlFlags flag) { return () => { @@ -671,35 +691,35 @@ private Func CreateControlFlagMap(TerrainControlFlags flag) return; } - bmp.SetPixel(x, y, Color.White); + bmp.SetPixel(x, y, SKColors.White); }); }; } - private Bitmap CreateControlMap() + private SKBitmap CreateControlMap() { return _zone.CreateBitmap().ForEach((bmp, x, y) => { TerrainControlInfo control = _zone.Terrain.Controls.GetValue(x, y); - int c = (int)control.Flags; - bmp.SetPixel(x, y, Color.FromArgb(c, c, c)); + byte c = (byte)control.Flags; + bmp.SetPixel(x, y, new(c, c, c)); }); } - private Bitmap CreateGroundTypeMap() + private SKBitmap CreateGroundTypeMap() { int numGroundTypes = Enum.GetNames(typeof(GroundType)).Length; - Color[] colors = new Color[numGroundTypes]; + SKColor[] colors = new SKColor[numGroundTypes]; Random random = new(numGroundTypes); for (int i = 0; i < colors.Length; i++) { - colors[i] = Color.FromArgb(random.Next(255), random.Next(255), random.Next(255)); + colors[i] = new((byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255)); } return _zone.CreateBitmap().ForEach((bmp, x, y) => { GroundType groundType = _zone.Terrain.Plants.GetValue(x, y).groundType; - Color c = colors[((int)groundType).Clamp(0, numGroundTypes - 1)]; + SKColor c = colors[((int)groundType).Clamp(0, numGroundTypes - 1)]; bmp.SetPixel(x, y, c); }); } @@ -714,9 +734,9 @@ private void CreateMineralBitmaps() } [CanBeNull] - private Bitmap CreateMineralsToNormalizedBitmap(MineralLayer layer) + private SKBitmap CreateMineralsToNormalizedBitmap(MineralLayer layer) { - Bitmap bitmap = _zone.CreatePassableBitmap(_passableColor); + SKBitmap bitmap = _zone.CreatePassableBitmap(_passableColor); foreach (MineralNode node in layer.Nodes) { @@ -729,24 +749,26 @@ private Bitmap CreateMineralsToNormalizedBitmap(MineralLayer layer) double n = (double)node.GetValue(x, y) / maxAmount; if (n > 0.0) { - int c = (int)(n * 255); - bitmap.SetPixel(x, y, Color.FromArgb(c, 0, 0)); + byte c = (byte)(n * 255); + bitmap.SetPixel(x, y, new(c, 0, 0)); } } } } - return bitmap.WithGraphics(g => + return bitmap.WithCanvas(c => { string infoString = $"{layer.Type}"; - g.DrawString(infoString, new Font("Tahoma", 10), Brushes.White, 10, 10); + var paint = new SKPaint { Color = SKColors.White }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 10); + c.DrawText(infoString, 10, 10 + font.Size, font, paint); }); } private void CreateTeleportDecorMaps() { - CreateTeleportDecorMaps(out Bitmap bitmap, out Bitmap circlesBitmap); + CreateTeleportDecorMaps(out SKBitmap bitmap, out SKBitmap circlesBitmap); CreateAndSave("teleportdecor", () => bitmap); CreateAndSave("teleportdecor_circles", () => circlesBitmap); @@ -756,7 +778,7 @@ private void CreateTeleportDecorMaps() /// This function creates the blend map on gamma islands around the teleports /// Finds the farthest decor tile and draws a smooth circle gradient around the teleport /// - private void CreateTeleportDecorMaps(out Bitmap? bitmap, out Bitmap? circlesBitmap) + private void CreateTeleportDecorMaps(out SKBitmap? bitmap, out SKBitmap? circlesBitmap) { bitmap = null; circlesBitmap = null; @@ -768,12 +790,12 @@ private void CreateTeleportDecorMaps(out Bitmap? bitmap, out Bitmap? circlesBitm ITerrain terrain = _zone.Terrain; bitmap = _zone.CreateBitmap(); - Color color = Color.MediumVioletRed; - Font font = new("Tahoma", 8); - Graphics graphics = Graphics.FromImage(bitmap); + SKColor color = SKColors.MediumVioletRed; + SKFont font = new(SKTypeface.FromFamilyName("Tahoma"), 8); + SKCanvas canvas = new(bitmap); circlesBitmap = _zone.CreateBitmap(); - Graphics circlesGraphics = Graphics.FromImage(circlesBitmap); + SKCanvas circlesGraphics = new(circlesBitmap); ushort[] blendData = _zone.Size.CreateArray(); @@ -783,7 +805,7 @@ private void CreateTeleportDecorMaps(out Bitmap? bitmap, out Bitmap? circlesBitm double maximumDistance = 0.0; - Bitmap tmpBmp = bitmap; + SKBitmap tmpBmp = bitmap; area.ForEachXY((x, y) => { if (x < 0 || x >= _zone.Size.Width || y < 0 || y >= _zone.Size.Height) @@ -806,14 +828,17 @@ private void CreateTeleportDecorMaps(out Bitmap? bitmap, out Bitmap? circlesBitm tmpBmp.SetPixel(x, y, color); }); - graphics.DrawString(maximumDistance.ToString(CultureInfo.InvariantCulture), font, Brushes.White, (float)td.CurrentPosition.X, (float)td.CurrentPosition.Y); + var textPaint = new SKPaint { Color = SKColors.White }; + canvas.DrawText(maximumDistance.ToString(CultureInfo.InvariantCulture), (float)td.CurrentPosition.X, (float)td.CurrentPosition.Y + font.Size, font, textPaint); if (maximumDistance <= 0) { continue; } - circlesGraphics.FillEllipse(Brushes.White, (float)(td.CurrentPosition.intX - maximumDistance), (float)(td.CurrentPosition.intY - maximumDistance), (float)(maximumDistance * 2), (float)(maximumDistance * 2)); + var paint = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Fill }; + var rect = SKRect.Create((float)(td.CurrentPosition.intX - maximumDistance), (float)(td.CurrentPosition.intY - maximumDistance), (float)(maximumDistance * 2), (float)(maximumDistance * 2)); + circlesGraphics.DrawOval(rect, paint); Area tpArea = Area.FromRadius(td.CurrentPosition, (int)maximumDistance + 200); tpArea.ForEachXY((x, y) => @@ -850,39 +875,39 @@ private void CreateMissionMapByLevels() - private Bitmap DrawMissionByLevels() + private SKBitmap DrawMissionByLevels() { - Bitmap b = CreateAltitudeBitmap(); + SKBitmap b = CreateAltitudeBitmap(); DrawPixels(b); - b.WithGraphics(DrawLayers); + b.WithCanvas(DrawLayers); return b; } - private void DrawLayers(Graphics g) + private void DrawLayers(SKCanvas g) { DrawStringTopLeft(g, "valami cucc rajta"); //... tobbi graphics piszkalo } - private void DrawPixels(Bitmap bitmap) + private void DrawPixels(SKBitmap bitmap) { DrawPassableInGreen(bitmap); //... tobbi bitmap piszkalo } - private void DrawPassableInGreen(Bitmap bmp) + private void DrawPassableInGreen(SKBitmap bmp) { bmp.ForEach((b, x, y) => { - Color blockedColor = Color.FromArgb(255, 0, 0, 0); - Color passableColor = Color.FromArgb(255, 60, 60, 60); + SKColor blockedColor = new(0, 0, 0); + SKColor passableColor = new(60, 60, 60); if (_zone.Terrain.IsPassable(new Position(x, y))) { @@ -896,9 +921,11 @@ private void DrawPassableInGreen(Bitmap bmp) } - private void DrawStringTopLeft(Graphics graphics, string text) + private void DrawStringTopLeft(SKCanvas canvas, string text) { - graphics.DrawString(text, new Font("Tahoma", 20), Brushes.Chocolate, new PointF(50, 100)); + var paint = new SKPaint { Color = SKColors.Chocolate }; + var font = new SKFont(SKTypeface.FromFamilyName("Tahoma"), 20); + canvas.DrawText(text, 50, 100 + font.Size, font, paint); } private void SendDrawFunctionFinished(IRequest request) diff --git a/src/Perpetuum.RequestHandlers/Zone/ZoneSetLayerWithBitMap.cs b/src/Perpetuum.RequestHandlers/Zone/ZoneSetLayerWithBitMap.cs index 6e5ddec8..4f8d6071 100644 --- a/src/Perpetuum.RequestHandlers/Zone/ZoneSetLayerWithBitMap.cs +++ b/src/Perpetuum.RequestHandlers/Zone/ZoneSetLayerWithBitMap.cs @@ -1,8 +1,8 @@ -using System.Drawing; -using Perpetuum.Host.Requests; +using Perpetuum.Host.Requests; using Perpetuum.IO; using Perpetuum.Zones; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.RequestHandlers.Zone { @@ -22,12 +22,12 @@ public void HandleRequest(IZoneRequest request) var flagValue = request.Data.GetOrDefault(k.flags); var controlFlag = EnumHelper.GetEnum(flagValue); var path = _fileSystem.CreatePath("bitmaps", zone.CreateTerrainDataFilename(fileName, "png")); - var img = Image.FromFile(path); - using (Bitmap bmp = new Bitmap(img)) + var img = SKImage.FromEncodedData(path); + using (SKBitmap bmp = SKBitmap.FromImage(img)) { zone.Terrain.Controls.UpdateAll((x, y, c) => { - if (bmp.GetPixel(x, y).A == 0) + if (bmp.GetPixel(x, y).Alpha == 0) { return c; } diff --git a/src/Perpetuum.Server/Program.cs b/src/Perpetuum.Server/Program.cs index 84fb80a0..d828f3d2 100644 --- a/src/Perpetuum.Server/Program.cs +++ b/src/Perpetuum.Server/Program.cs @@ -37,7 +37,8 @@ static int Main(string[] args) return 3; } - bootstrapper.Init(gameRoot.Value); + // When using PerpetuumServer, the DistributedTransactions is enabled + bootstrapper.Init(gameRoot.Value, true); if (bootstrapper.TryInitUpnp(out bool upnpSuccess)) { diff --git a/src/Perpetuum.ServerService2/PerpetuumServerService2.cs b/src/Perpetuum.ServerService2/PerpetuumServerService2.cs index 1d4dd3ed..9dc56306 100644 --- a/src/Perpetuum.ServerService2/PerpetuumServerService2.cs +++ b/src/Perpetuum.ServerService2/PerpetuumServerService2.cs @@ -23,11 +23,12 @@ public void ServerStart() { // assumes the server is in the default installation directory. string gameroot = _configuration.GetValue("GameRoot") ?? "C:\\PerpetuumServer\\data"; + bool distributedTransactions = _configuration.GetValue("DistributedTransactions", true); _logger.LogInformation("Perpetuum Dedicated Server v2 - starting"); try { - Bootstrapper.Init(gameroot); + Bootstrapper.Init(gameroot, distributedTransactions); } catch (Exception ex) { diff --git a/src/Perpetuum.Tests.Integration/Content/DefinitionTintInvariantTests.cs b/src/Perpetuum.Tests.Integration/Content/DefinitionTintInvariantTests.cs new file mode 100644 index 00000000..7bb94519 --- /dev/null +++ b/src/Perpetuum.Tests.Integration/Content/DefinitionTintInvariantTests.cs @@ -0,0 +1,43 @@ +using Microsoft.Data.SqlClient; +using Perpetuum.Tests.Integration.Infrastructure; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Integration.Content +{ + [Collection(DatabaseCollection.Name)] + public class DefinitionTintInvariantTests + { + [RequiresGameRootFact] + public void Every_definition_tint_in_the_database_is_parseable_by_SkiaSharp() + { + DatabaseFixture fixture = new(); + using SqlConnection connection = fixture.OpenConnection(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = """ + SELECT definition, tint + FROM dbo.definitionconfig + WHERE tint IS NOT NULL AND LTRIM(RTRIM(tint)) <> '' + """; + + using SqlDataReader reader = command.ExecuteReader(); + List unparseable = []; + + while (reader.Read()) + { + int definition = reader.GetInt32(0); + string tint = reader.GetString(1); + + if (!SKColor.TryParse(tint, out _)) + { + unparseable.Add($"Definition {definition}: '{tint}'"); + } + } + + Assert.True( + unparseable.Count == 0, + $"The following definitionconfig rows carry invalid/unparseable tint values for SkiaSharp: {string.Join(", ", unparseable)}"); + } + } +} diff --git a/src/Perpetuum.Tests.Integration/Data/DistributedTransactionTests.cs b/src/Perpetuum.Tests.Integration/Data/DistributedTransactionTests.cs new file mode 100644 index 00000000..0ec248a4 --- /dev/null +++ b/src/Perpetuum.Tests.Integration/Data/DistributedTransactionTests.cs @@ -0,0 +1,130 @@ +using System.Data; +using System.Transactions; +using Microsoft.Data.SqlClient; +using Perpetuum.Data; +using Perpetuum.Tests.Integration.Infrastructure; +using Xunit; + +namespace Perpetuum.Tests.Integration.Data +{ + /// + /// Executes the DistributedTransactions = true branch of DbQuery.ExecuteHelper against the + /// real database. That branch is the production Windows path (Perpetuum.Server enables the + /// flag in Program.cs) and it was previously exercised only by fakes, which record the + /// EnlistTransaction call without ever handing a connection to a transaction manager. + /// + /// The rollback test is the diagnostic one: if enlistment ever stops happening (flag + /// regression, "connection is DbConnection" check broken, wrong configuration object), the + /// inserts auto-commit and the count after the aborted scope is 2 instead of 0. + /// + /// Writes are gated on PERPETUUM_TESTDB_ALLOW_WRITE=1. Each test uses a uniquely named + /// scratch table it creates and drops itself, so a leftover table from a crashed run cannot + /// pollute a later one. + /// + [Collection(DatabaseCollection.Name)] + public class DistributedTransactionTests + { + private readonly DatabaseFixture _fixture = new(); + + private static DbQuery NewQuery(string connectionString) + { + // A fresh factory per query, exactly as production builds them. Inside a scope the + // factory runs once and DbConnectionManager shares that connection; EnlistTransaction + // on a connection already enlisted in the same transaction is a no-op in + // Microsoft.Data.SqlClient, so the per-query call in DbQuery is safe. + return new DbQuery( + () => new SqlConnection(connectionString), + new GlobalConfiguration { DistributedTransactions = true }); + } + + private static string NewScratchTable() + => "dbo.op_test_dtc_" + Guid.NewGuid().ToString("N")[..8]; + + private void CreateScratchTable(string table) + { + using SqlConnection connection = _fixture.OpenConnection(); + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"create table {table} (value int not null)"; + command.ExecuteNonQuery(); + } + + private void DropScratchTable(string table) + { + using SqlConnection connection = _fixture.OpenConnection(); + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"drop table if exists {table}"; + command.ExecuteNonQuery(); + } + + private int CountRows(string table) + { + using SqlConnection connection = _fixture.OpenConnection(); + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"select count(*) from {table}"; + return (int)command.ExecuteScalar(); + } + + [RequiresWriteDatabaseFact] + public void Enlisted_queries_rollback_when_the_scope_aborts() + { + string table = NewScratchTable(); + try + { + CreateScratchTable(table); + + DbQuery insert1 = NewQuery(_fixture.LocalEnvironment!.ConnectionString); + DbQuery insert2 = NewQuery(_fixture.LocalEnvironment.ConnectionString); + + using (TransactionScope scope = new()) + { + insert1.CommandText($"insert into {table} values (1)").ExecuteNonQuery(); + insert2.CommandText($"insert into {table} values (2)").ExecuteNonQuery(); + + // One shared connection per scope means one enlisted resource, which is what + // keeps the scope local. Escalation to MSDTC would throw on Linux, where no + // coordinator exists, so succeeding is also the no-escalation check. + var current = Transaction.Current!; + Assert.Equal(TransactionStatus.Active, current.TransactionInformation.Status); + Assert.Equal(1, DbConnectionManager.ActiveConnectionCount); + } + // No Complete: the scope aborts. + + Assert.Equal(0, CountRows(table)); + } + finally + { + DropScratchTable(table); + } + + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + } + + [RequiresWriteDatabaseFact] + public void Enlisted_queries_commit_when_the_scope_completes() + { + string table = NewScratchTable(); + try + { + CreateScratchTable(table); + + DbQuery insert1 = NewQuery(_fixture.LocalEnvironment!.ConnectionString); + DbQuery insert2 = NewQuery(_fixture.LocalEnvironment.ConnectionString); + + using (TransactionScope scope = new()) + { + insert1.CommandText($"insert into {table} values (1)").ExecuteNonQuery(); + insert2.CommandText($"insert into {table} values (2)").ExecuteNonQuery(); + scope.Complete(); + } + + Assert.Equal(2, CountRows(table)); + } + finally + { + DropScratchTable(table); + } + + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + } + } +} diff --git a/src/Perpetuum.Tests.Integration/Infrastructure/RequiresWriteDatabaseFactAttribute.cs b/src/Perpetuum.Tests.Integration/Infrastructure/RequiresWriteDatabaseFactAttribute.cs new file mode 100644 index 00000000..3fec373e --- /dev/null +++ b/src/Perpetuum.Tests.Integration/Infrastructure/RequiresWriteDatabaseFactAttribute.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace Perpetuum.Tests.Integration.Infrastructure +{ + /// + /// Like RequiresGameRootFactAttribute, plus the write opt-in: a test marked with this + /// attribute is skipped, not failed, when the local environment is absent or when + /// PERPETUUM_TESTDB_ALLOW_WRITE is not set to 1. This is the first attribute in the project + /// that admits writing; existing tests stay read-only by default. + /// + public sealed class RequiresWriteDatabaseFactAttribute : FactAttribute + { + public RequiresWriteDatabaseFactAttribute() + { + if (!GameRootEnvironment.TryLoad(out _, out string? reason)) + { + Skip = $"Local game environment unavailable: {reason}"; + } + else if (!GameRootEnvironment.WritesAllowed) + { + Skip = $"{GameRootEnvironment.AllowWriteVariable} is not set to 1; this test writes to the database."; + } + } + } +} diff --git a/src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj b/src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj index b7496cdb..5b704386 100644 --- a/src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj +++ b/src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj @@ -12,10 +12,13 @@ + + + diff --git a/src/Perpetuum.Tests.Integration/Skia/BitmapImageIntegrationTests.cs b/src/Perpetuum.Tests.Integration/Skia/BitmapImageIntegrationTests.cs new file mode 100644 index 00000000..41809865 --- /dev/null +++ b/src/Perpetuum.Tests.Integration/Skia/BitmapImageIntegrationTests.cs @@ -0,0 +1,192 @@ +using NSubstitute; +using Perpetuum.Host.Requests; +using Perpetuum.IO; +using Perpetuum.RequestHandlers.Zone; +using Perpetuum.Zones; +using Perpetuum.Zones.Terrains; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Integration.Skia +{ + public class BitmapImageIntegrationTests : IDisposable + { + private readonly string _tempDirectory; + + public BitmapImageIntegrationTests() + { + _tempDirectory = Path.Combine(Path.GetTempPath(), "opp_test_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path.Combine(_tempDirectory, "bitmaps")); + Message.MessageBuilderFactory = () => new MessageBuilder(null, Substitute.For(), null); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + try + { + Directory.Delete(_tempDirectory, recursive: true); + } + catch + { + // Ignored on cleanup + } + } + } + + [Fact] + public void SKBitmap_and_SKCanvas_encode_to_PNG_and_decode_accurately() + { + const int width = 16; + const int height = 16; + using var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + + bitmap.WithCanvas(canvas => + { + canvas.Clear(SKColors.Transparent); + + using var redPaint = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Fill }; + canvas.DrawRect(0, 0, 8, 8, redPaint); + + using var bluePaint = new SKPaint { Color = SKColors.Blue, Style = SKPaintStyle.Fill }; + canvas.DrawRect(8, 8, 8, 8, bluePaint); + }); + + // Encode to PNG stream + using var ms = new MemoryStream(); + bool encoded = bitmap.Encode(ms, SKEncodedImageFormat.Png, 100); + Assert.True(encoded); + Assert.True(ms.Length > 0); + + // Decode back with SKImage + ms.Position = 0; + using var img = SKImage.FromEncodedData(ms); + Assert.NotNull(img); + Assert.Equal(width, img.Width); + Assert.Equal(height, img.Height); + + using var decodedBmp = SKBitmap.FromImage(img); + Assert.NotNull(decodedBmp); + Assert.Equal(SKColors.Red, decodedBmp.GetPixel(4, 4)); + Assert.Equal(SKColors.Blue, decodedBmp.GetPixel(12, 12)); + Assert.Equal(SKColors.Empty, decodedBmp.GetPixel(12, 4)); // Transparent + } + + [Fact] + public void SaveBitmapHelper_writes_valid_PNG_to_filesystem() + { + var fileSystem = new FileSystem(_tempDirectory); + var zone = Substitute.For(); + zone.Id.Returns(1); + + var expectedFileName = zone.CreateTerrainDataFilename("stat_map", "png"); + var expectedFilePath = Path.Combine(_tempDirectory, "bitmaps", expectedFileName); + + using var bmp = new SKBitmap(8, 8, SKColorType.Rgba8888, SKAlphaType.Premul); + bmp.WithCanvas(c => c.Clear(SKColors.Green)); + + var helper = new SaveBitmapHelper(fileSystem); + helper.SaveBitmap(zone, bmp, "stat_map"); + + Assert.True(File.Exists(expectedFilePath)); + + using var readImg = SKImage.FromEncodedData(expectedFilePath); + Assert.NotNull(readImg); + using var readBmp = SKBitmap.FromImage(readImg); + Assert.Equal(SKColors.Green, readBmp.GetPixel(0, 0)); + Assert.Equal(SKColors.Green, readBmp.GetPixel(7, 7)); + } + + [Fact] + public void ZoneExtensions_CreatePassableBitmap_generates_correct_pixel_map() + { + var zone = Substitute.For(); + zone.Size.Returns(new SKSizeI(4, 4)); + + var blocks = new Layer(LayerType.Blocks, 4, 4); + var altitude = new AltitudeLayer(new ushort[4 * 4], 4, 4); + var slope = new SlopeLayer(altitude); + + var terrain = Substitute.For(); + terrain.Passable.Returns((ILayer)null!); + terrain.Blocks.Returns(blocks); + terrain.Slope.Returns(slope); + + // (0,0) is island + blocks[0, 0] = new BlockingInfo { Island = true }; + // (1,1) is passable (Flags = 0) + blocks[1, 1] = new BlockingInfo(); + // (2,2) is impassable/blocked (Flags != 0) + blocks[2, 2] = new BlockingInfo { Obstacle = true }; + + zone.Terrain.Returns(terrain); + + var passableColor = SKColors.Lime; + var islandColor = SKColors.Yellow; + + using var bmp = zone.CreatePassableBitmap(passableColor, islandColor); + + Assert.NotNull(bmp); + Assert.Equal(4, bmp.Width); + Assert.Equal(4, bmp.Height); + + Assert.Equal(islandColor, bmp.GetPixel(0, 0)); + Assert.Equal(passableColor, bmp.GetPixel(1, 1)); + Assert.Equal(SKColors.Black, bmp.GetPixel(2, 2)); + } + + [Fact] + public void ZoneSetLayerWithBitMap_applies_mask_from_PNG_image() + { + var fileSystem = new FileSystem(_tempDirectory); + var zone = Substitute.For(); + zone.Id.Returns(1); + zone.IsLayerEditLocked.Returns(false); + + var maskFileName = zone.CreateTerrainDataFilename("mask", "png"); + var maskPath = Path.Combine(_tempDirectory, "bitmaps", maskFileName); + + using (var maskBmp = new SKBitmap(4, 4, SKColorType.Rgba8888, SKAlphaType.Premul)) + { + maskBmp.WithCanvas(c => + { + c.Clear(SKColors.Transparent); + using var p = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Fill }; + c.DrawRect(0, 0, 2, 2, p); + }); + using var fs = File.Create(maskPath); + maskBmp.Encode(fs, SKEncodedImageFormat.Png, 100); + } + + var controls = new Layer(LayerType.Control, 4, 4); + var terrain = Substitute.For(); + terrain.Controls.Returns(controls); + + zone.Terrain.Returns(terrain); + + var session = Substitute.For(); + + var request = Substitute.For(); + request.Zone.Returns(zone); + request.Session.Returns(session); + var requestData = new Dictionary + { + { k.file, "mask" }, + { k.flags, (int)TerrainControlFlags.SyndicateArea } + }; + request.Data.Returns(requestData); + + var handler = new ZoneSetLayerWithBitMap(fileSystem); + handler.HandleRequest(request); + + // Opaque area (0,0), (0,1), (1,0), (1,1) should have SyndicateArea flag set + Assert.True(controls[0, 0].SyndicateArea); + Assert.True(controls[1, 1].SyndicateArea); + + // Transparent area (2,2), (3,3) should remain not SyndicateArea + Assert.False(controls[2, 2].SyndicateArea); + Assert.False(controls[3, 3].SyndicateArea); + } + } +} diff --git a/src/Perpetuum.Tests/Fakes/Data/EnlistableFakeDbConnection.cs b/src/Perpetuum.Tests/Fakes/Data/EnlistableFakeDbConnection.cs new file mode 100644 index 00000000..e1d18f9f --- /dev/null +++ b/src/Perpetuum.Tests/Fakes/Data/EnlistableFakeDbConnection.cs @@ -0,0 +1,233 @@ +using System.Collections; +using System.Data; +using System.Data.Common; +using System.Transactions; + +namespace Perpetuum.Tests.Fakes.Data +{ + /// + /// The opposite of FakeDbConnection: it derives from DbConnection so it reaches the + /// DistributedTransactions = true branch in DbQuery.ExecuteHelper, where the ambient + /// transaction is enlisted via DbConnection.EnlistTransaction. The override here is a + /// recording no-op, so the fake stays out of any transaction manager. State and command + /// execution are delegated to a wrapped FakeDbConnection/FakeDbCommand, so stubs + /// registered on the FakeDb and the command recording behave exactly as in the other + /// unit tests. + /// + public sealed class EnlistableFakeDbConnection(FakeDb owner) : DbConnection + { + private readonly FakeDbConnection _inner = new(owner); + + public int EnlistmentCount { get; private set; } + public Transaction? EnlistedTransaction { get; private set; } + + public override void EnlistTransaction(Transaction transaction) + { + EnlistmentCount++; + EnlistedTransaction = transaction; + } + + public override string ConnectionString + { + get => _inner.ConnectionString; + set => _inner.ConnectionString = value; + } + + public override int ConnectionTimeout => _inner.ConnectionTimeout; + public override string Database => _inner.Database; + public override string DataSource => "fake"; + public override string ServerVersion => "fake"; + public override ConnectionState State => _inner.State; + + public override void ChangeDatabase(string databaseName) => _inner.ChangeDatabase(databaseName); + public override void Close() => _inner.Close(); + public override void Open() => _inner.Open(); + + protected override DbCommand CreateDbCommand() + => new EnlistableFakeDbCommand(this, _inner); + + protected override DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel) + => throw new NotSupportedException(); + } + + /// + /// DbConnection.CreateDbCommand must return a DbCommand, while the shared FakeDbCommand only + /// implements IDbCommand. This adapter satisfies the DbCommand surface DbQuery uses and + /// forwards execution to a FakeDbCommand proxy, so result matching and command recording go + /// through the same path as every other unit test. + /// + internal sealed class EnlistableFakeDbCommand(EnlistableFakeDbConnection connection, FakeDbConnection inner) + : DbCommand + { + private readonly FakeDbCommand _proxy = new(inner); + + public override string CommandText + { + get => _proxy.CommandText; + set => _proxy.CommandText = value; + } + + public override int CommandTimeout + { + get => _proxy.CommandTimeout; + set => _proxy.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => _proxy.CommandType; + set => _proxy.CommandType = value; + } + + // The class-level property names in DbCommand avoid the type-name clash this way. + protected override DbConnection DbConnection { get; set; } = connection; + protected override DbTransaction DbTransaction { get; set; } + public override bool DesignTimeVisible { get; set; } + protected override DbParameterCollection DbParameterCollection { get; } = new SimpleDbParameterCollection(); + public override UpdateRowSource UpdatedRowSource { get; set; } + + protected override DbParameter CreateDbParameter() + => new SimpleDbParameter(); + + public override void Cancel() { } + public override void Prepare() { } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + SyncParameters(); + return new EnlistableFakeDbDataReader(_proxy.ExecuteReader(behavior)); + + } + + public override int ExecuteNonQuery() + { + SyncParameters(); + return _proxy.ExecuteNonQuery(); + } + + public override object? ExecuteScalar() + { + SyncParameters(); + return _proxy.ExecuteScalar(); + } + + private void SyncParameters() + { + foreach (DbParameter parameter in DbParameterCollection) + { + _proxy.Parameters.Add(new FakeParameter + { + ParameterName = parameter.ParameterName, + Value = parameter.Value, + }); + } + } + } + + /// + /// DbCommand.ExecuteDbDataReader must return a DbDataReader; this wraps the shared + /// FakeDataReader, which only implements IDataReader. + /// + internal sealed class EnlistableFakeDbDataReader(IDataReader inner) : DbDataReader + { + private readonly IDataReader _inner = inner; + + public override bool HasRows => true; + public override int Depth => _inner.Depth; + public override int FieldCount => _inner.FieldCount; + public override bool IsClosed => _inner.IsClosed; + public override int RecordsAffected => _inner.RecordsAffected; + public override object this[int i] => _inner[i]; + public override object this[string name] => _inner[name]; + + public override string GetDataTypeName(int i) => _inner.GetDataTypeName(i); + public override IEnumerator GetEnumerator() => ((IEnumerable)_inner).GetEnumerator(); + public override Type GetFieldType(int i) => _inner.GetFieldType(i); + public override string GetName(int i) => _inner.GetName(i); + public override int GetOrdinal(string name) => _inner.GetOrdinal(name); + + public override bool GetBoolean(int i) => _inner.GetBoolean(i); + public override byte GetByte(int i) => _inner.GetByte(i); + public override long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) + => _inner.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + public override char GetChar(int i) => _inner.GetChar(i); + public override long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) + => _inner.GetChars(i, fieldoffset, buffer, bufferoffset, length); + public override DateTime GetDateTime(int i) => _inner.GetDateTime(i); + public override decimal GetDecimal(int i) => _inner.GetDecimal(i); + public override double GetDouble(int i) => _inner.GetDouble(i); + public override float GetFloat(int i) => _inner.GetFloat(i); + public override Guid GetGuid(int i) => _inner.GetGuid(i); + public override short GetInt16(int i) => _inner.GetInt16(i); + public override int GetInt32(int i) => _inner.GetInt32(i); + public override long GetInt64(int i) => _inner.GetInt64(i); + public override string GetString(int i) => _inner.GetString(i); + public override object GetValue(int i) => _inner.GetValue(i); + public override int GetValues(object[] values) => _inner.GetValues(values); + public override bool IsDBNull(int i) => _inner.IsDBNull(i); + public override bool NextResult() => _inner.NextResult(); + public override bool Read() => _inner.Read(); + + // DbDataReader.Dispose is non-virtual and routes to Close. + public override void Close() => _inner.Dispose(); + } + + /// + /// DbParameterCollection is abstract; this stores parameters in a list. DbQuery only uses + /// Add, enumeration and the indexer through the interface surface. + /// + internal sealed class SimpleDbParameterCollection : DbParameterCollection + { + private readonly List _parameters = []; + + private static DbParameter AsParameter(object value) + => value as DbParameter + ?? throw new InvalidCastException($"Expected DbParameter, got {value?.GetType().Name ?? "null"}."); + + public override bool Contains(object value) => _parameters.Contains(value); + public override bool Contains(string parameterName) + => _parameters.Cast().Any(p => p.ParameterName == parameterName); + public override bool IsReadOnly => false; + public override bool IsSynchronized => false; + public override int Count => _parameters.Count; + public override object SyncRoot => ((System.Collections.ICollection)_parameters).SyncRoot; + public override IEnumerator GetEnumerator() => _parameters.GetEnumerator(); + + public override int Add(object value) { _parameters.Add(value); return _parameters.Count - 1; } + public override void AddRange(Array values) { foreach (object value in values) { Add(value); } } + public override void Clear() => _parameters.Clear(); + public override void CopyTo(Array array, int index) => ((ICollection)_parameters).CopyTo(array, index); + protected override DbParameter GetParameter(int index) => AsParameter(_parameters[index]); + protected override DbParameter GetParameter(string parameterName) + => _parameters.Cast().First(p => p.ParameterName == parameterName); + public override int IndexOf(object value) => _parameters.IndexOf(value); + public override int IndexOf(string parameterName) + => _parameters.FindIndex(p => ReferenceEquals(AsParameter(p).ParameterName, parameterName)); + public override void Insert(int index, object value) => _parameters.Insert(index, value); + public override void Remove(object value) => _parameters.Remove(value); + public override void RemoveAt(int index) => _parameters.RemoveAt(index); + public override void RemoveAt(string parameterName) + => RemoveAt(IndexOf(parameterName)); + protected override void SetParameter(int index, DbParameter value) => _parameters[index] = value; + protected override void SetParameter(string parameterName, DbParameter value) + => _parameters[IndexOf(parameterName)] = value; + } + + /// Minimal concrete DbParameter; DbQuery only sets ParameterName and Value on it. + internal sealed class SimpleDbParameter : DbParameter + { + public override DbType DbType { get; set; } + public override ParameterDirection Direction { get; set; } + public override bool IsNullable { get; set; } + public override string ParameterName { get; set; } = string.Empty; + public override byte Precision { get; set; } + public override byte Scale { get; set; } + public override int Size { get; set; } + public override string SourceColumn { get; set; } = string.Empty; + public override bool SourceColumnNullMapping { get; set; } + public override DataRowVersion SourceVersion { get; set; } + public override object? Value { get; set; } + + public override void ResetDbType() => DbType = DbType.Object; + } +} diff --git a/src/Perpetuum.Tests/Fakes/Data/FakeDb.cs b/src/Perpetuum.Tests/Fakes/Data/FakeDb.cs index 3faf9c95..41b406fa 100644 --- a/src/Perpetuum.Tests/Fakes/Data/FakeDb.cs +++ b/src/Perpetuum.Tests/Fakes/Data/FakeDb.cs @@ -32,7 +32,9 @@ public sealed class FakeDb public static FakeDb Install() { FakeDb fake = new(); - Db.DbQueryFactory = () => new DbQuery(() => new FakeDbConnection(fake)); + Db.DbQueryFactory = () => new DbQuery( + () => new FakeDbConnection(fake), + new GlobalConfiguration { DistributedTransactions = false }); return fake; } diff --git a/src/Perpetuum.Tests/Perpetuum.Tests.csproj b/src/Perpetuum.Tests/Perpetuum.Tests.csproj index 888e45fc..5b704386 100644 --- a/src/Perpetuum.Tests/Perpetuum.Tests.csproj +++ b/src/Perpetuum.Tests/Perpetuum.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -13,6 +13,7 @@ + diff --git a/src/Perpetuum.Tests/Unit/AreaTests.cs b/src/Perpetuum.Tests/Unit/AreaTests.cs new file mode 100644 index 00000000..cabec175 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/AreaTests.cs @@ -0,0 +1,182 @@ +using Perpetuum.Zones; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class AreaTests + { + [Fact] + public void Constructor_normalizes_coordinates_when_inverted() + { + var a = new Area(10, 20, 5, 8); + Assert.Equal(5, a.X1); + Assert.Equal(8, a.Y1); + Assert.Equal(10, a.X2); + Assert.Equal(20, a.Y2); + Assert.Equal(6, a.Width); + Assert.Equal(13, a.Height); + } + + [Fact] + public void FromRectangle_sets_coordinates() + { + var a = Area.FromRectangle(10, 20, 30, 40); + Assert.Equal(10, a.X1); + Assert.Equal(20, a.Y1); + Assert.Equal(39, a.X2); + Assert.Equal(59, a.Y2); + Assert.Equal(30, a.Width); + Assert.Equal(40, a.Height); + Assert.Equal(1200, a.Ground); + Assert.Equal(50.0, a.Diagonal); + } + + [Fact] + public void FromRadius_with_SKPointI_and_Position() + { + var pt = new SKPointI(50, 50); + var a1 = Area.FromRadius(pt, 10); + Assert.Equal(40, a1.X1); + Assert.Equal(40, a1.Y1); + Assert.Equal(60, a1.X2); + Assert.Equal(60, a1.Y2); + + var pos = new Position(50, 50); + var a2 = Area.FromRadius(pos, 10); + Assert.Equal(a1, a2); + + var a3 = Area.FromRadius(50, 50, 10); + Assert.Equal(a1, a3); + } + + [Fact] + public void Center_and_CenterPrecise() + { + var a = new Area(0, 0, 10, 10); + Assert.Equal(new SKPointI(5, 5), a.Center); + Assert.Equal(new Position(5.0, 5.0), a.CenterPrecise); + } + + [Fact] + public void Contains_checks_point_and_area() + { + var a = new Area(10, 10, 20, 20); + + Assert.True(a.Contains(new SKPointI(10, 10))); + Assert.True(a.Contains(new SKPointI(15, 15))); + Assert.True(a.Contains(new SKPointI(20, 20))); + Assert.True(a.Contains(new Position(15, 15))); + + Assert.False(a.Contains(new SKPointI(9, 15))); + Assert.False(a.Contains(new SKPointI(15, 21))); + + var subArea = new Area(12, 12, 18, 18); + Assert.True(a.Contains(subArea)); + + var overlappingArea = new Area(15, 15, 25, 25); + Assert.False(a.Contains(overlappingArea)); + } + + [Fact] + public void ContainsInInnerCircle_evaluates_circle() + { + var a = Area.FromRectangle(0, 0, 20, 20); + Assert.True(a.ContainsInInnerCircle(10, 10)); + Assert.True(a.ContainsInInnerCircle(10, 15)); + Assert.False(a.ContainsInInnerCircle(0, 0)); + } + + [Fact] + public void Clamp_SKSizeI_and_dimensions() + { + var a = new Area(-10, -5, 150, 250); + var clamped = a.Clamp(new SKSizeI(100, 200)); + + Assert.Equal(0, clamped.X1); + Assert.Equal(0, clamped.Y1); + Assert.Equal(99, clamped.X2); + Assert.Equal(199, clamped.Y2); + } + + [Fact] + public void IntersectsWith_and_Intersect() + { + var a = new Area(0, 0, 10, 10); + var b = new Area(5, 5, 15, 15); + var c = new Area(20, 20, 30, 30); + + Assert.True(a.IntersectsWith(b)); + Assert.False(a.IntersectsWith(c)); + + var intersection = a.Intersect(b); + Assert.Equal(new Area(5, 5, 10, 10), intersection); + + var noIntersection = a.Intersect(c); + Assert.Equal(Area.Empty, noIntersection); + } + + [Fact] + public void Union_combines_areas() + { + var a = new Area(0, 0, 10, 10); + var b = new Area(5, 5, 20, 20); + + var u = Area.Union(a, b); + Assert.Equal(new Area(0, 0, 20, 20), u); + } + + [Fact] + public void Slice_partitions_area() + { + var a = new Area(0, 0, 10, 10); + var slices = a.Slice(5).ToList(); + Assert.NotEmpty(slices); + foreach (var s in slices) + { + Assert.True(a.Contains(s)); + } + } + + [Fact] + public void AddBorder_expands_area() + { + var a = new Area(10, 10, 20, 20); + var expanded = a.AddBorder(2); + Assert.Equal(8, expanded.X1); + Assert.Equal(8, expanded.Y1); + Assert.Equal(22, expanded.X2); + Assert.Equal(22, expanded.Y2); + } + + [Fact] + public void Distance_and_SqrDistance_between_areas_and_points() + { + var a = new Area(0, 0, 10, 10); + var pt = new SKPointI(13, 0); + Assert.Equal(3.0, a.Distance(pt)); + Assert.Equal(9.0, a.SqrDistance(pt)); + + var b = new Area(14, 0, 20, 10); + Assert.Equal(4.0, a.Distance(b)); + Assert.Equal(16.0, a.SqrDistance(b)); + + var overlap = new Area(5, 5, 15, 15); + Assert.Equal(0.0, a.Distance(overlap)); + } + + [Fact] + public void Equality_and_ToString() + { + var a1 = new Area(1, 2, 3, 4); + var a2 = new Area(1, 2, 3, 4); + var a3 = new Area(1, 2, 3, 5); + + Assert.True(a1 == a2); + Assert.False(a1 != a2); + Assert.True(a1 != a3); + Assert.Equal(a1.GetHashCode(), a2.GetHashCode()); + Assert.Contains("X1 = 1", a1.ToString()); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/BinaryStreamSkiaTests.cs b/src/Perpetuum.Tests/Unit/BinaryStreamSkiaTests.cs new file mode 100644 index 00000000..9b544167 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/BinaryStreamSkiaTests.cs @@ -0,0 +1,58 @@ +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class BinaryStreamSkiaTests + { + [Fact] + public void AppendObject_SKColor_writes_3_bytes() + { + using var stream = new BinaryStream(); + var color = new SKColor(12, 34, 56, 255); + stream.AppendObject(color); + + byte[] bytes = stream.ToArray(); + Assert.Equal(3, bytes.Length); + Assert.Equal(12, bytes[0]); + Assert.Equal(34, bytes[1]); + Assert.Equal(56, bytes[2]); + } + + [Fact] + public void AppendPoint_SKPointI_writes_2_integers() + { + using var stream = new BinaryStream(); + var pt = new SKPointI(12345, 67890); + stream.AppendPoint(pt); + + stream.Position = 0; + int x = stream.ReadInt(); + int y = stream.ReadInt(); + + Assert.Equal(12345, x); + Assert.Equal(67890, y); + Assert.True(stream.AtEnd()); + } + + [Fact] + public void AppendArea_writes_4_integers() + { + using var stream = new BinaryStream(); + var area = new Area(10, 20, 30, 40); + stream.AppendArea(area); + + stream.Position = 0; + int x1 = stream.ReadInt(); + int y1 = stream.ReadInt(); + int x2 = stream.ReadInt(); + int y2 = stream.ReadInt(); + + Assert.Equal(10, x1); + Assert.Equal(20, y1); + Assert.Equal(30, x2); + Assert.Equal(40, y2); + Assert.True(stream.AtEnd()); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/BitmapExtensionsTests.cs b/src/Perpetuum.Tests/Unit/BitmapExtensionsTests.cs new file mode 100644 index 00000000..c70903dc --- /dev/null +++ b/src/Perpetuum.Tests/Unit/BitmapExtensionsTests.cs @@ -0,0 +1,69 @@ +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class BitmapExtensionsTests + { + [Fact] + public void WithCanvas_null_bitmap_returns_null() + { + SKBitmap? bitmap = null; + var result = bitmap.WithCanvas(_ => { }); + Assert.Null(result); + } + + [Fact] + public void WithCanvas_executes_action_and_modifies_bitmap() + { + using var bitmap = new SKBitmap(10, 10, SKColorType.Rgba8888, SKAlphaType.Premul); + var result = bitmap.WithCanvas(canvas => + { + using var paint = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Fill }; + canvas.DrawRect(0, 0, 10, 10, paint); + }); + + Assert.Same(bitmap, result); + Assert.Equal(SKColors.Red, bitmap.GetPixel(5, 5)); + } + + [Fact] + public void ForEach_null_bitmap_returns_null() + { + SKBitmap? bitmap = null; + var result = bitmap.ForEach((_, _, _) => { }); + Assert.Null(result); + } + + [Fact] + public void ForEach_visits_every_pixel_and_modifies_bitmap() + { + const int width = 4; + const int height = 3; + using var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + + int visitedCount = 0; + var visitedCoordinates = new List<(int X, int Y)>(); + + var result = bitmap.ForEach((bmp, x, y) => + { + visitedCount++; + visitedCoordinates.Add((x, y)); + bmp.SetPixel(x, y, new SKColor((byte)(x * 10), (byte)(y * 10), 0)); + }); + + Assert.Same(bitmap, result); + Assert.Equal(width * height, visitedCount); + + int index = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + Assert.Equal((x, y), visitedCoordinates[index++]); + Assert.Equal(new SKColor((byte)(x * 10), (byte)(y * 10), 0), bitmap.GetPixel(x, y)); + } + } + } + } +} diff --git a/src/Perpetuum.Tests/Unit/ColorExtensionsTests.cs b/src/Perpetuum.Tests/Unit/ColorExtensionsTests.cs new file mode 100644 index 00000000..c9ff8f92 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/ColorExtensionsTests.cs @@ -0,0 +1,37 @@ +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class ColorExtensionsTests + { + [Theory] + [InlineData(0, 0, 0, 0f)] + [InlineData(255, 255, 255, 1f)] + [InlineData(255, 0, 0, 0.299f)] + [InlineData(0, 255, 0, 0.587f)] + [InlineData(0, 0, 255, 0.114f)] + public void GetLuminance_primary_colors_and_extremes(byte r, byte g, byte b, float expected) + { + var color = new SKColor(r, g, b); + float luminance = color.GetLuminance(); + Assert.Equal(expected, luminance, precision: 4); + } + + [Fact] + public void GetLuminance_arbitrary_rgb() + { + var color = new SKColor(128, 64, 32); + float expected = (0.299f * 128 + 0.587f * 64 + 0.114f * 32) / 255f; + Assert.Equal(expected, color.GetLuminance(), precision: 5); + } + + [Fact] + public void GetLuminance_ignores_alpha() + { + var c1 = new SKColor(100, 150, 200, 255); + var c2 = new SKColor(100, 150, 200, 0); + Assert.Equal(c1.GetLuminance(), c2.GetLuminance()); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/ConnectionStringSupportTests.cs b/src/Perpetuum.Tests/Unit/ConnectionStringSupportTests.cs index 93f69273..4b2fbe61 100644 --- a/src/Perpetuum.Tests/Unit/ConnectionStringSupportTests.cs +++ b/src/Perpetuum.Tests/Unit/ConnectionStringSupportTests.cs @@ -95,7 +95,7 @@ public void Keyword_matching_ignores_case(string spelling) [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void Absent_or_blank_input_reports_nothing(string input) + public void Absent_or_blank_input_reports_nothing(string? input) { Assert.Empty(ConnectionStringSupport.FindUnsupportedKeywords(input)); } diff --git a/src/Perpetuum.Tests/Unit/DbConnectionManagerTests.cs b/src/Perpetuum.Tests/Unit/DbConnectionManagerTests.cs new file mode 100644 index 00000000..e326358d --- /dev/null +++ b/src/Perpetuum.Tests/Unit/DbConnectionManagerTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Data; +using System.Transactions; +using Perpetuum.Data; +using Perpetuum.Tests.Fakes.Data; +using Perpetuum.Tests.Infrastructure; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + [Collection(PerpetuumStaticsCollection.Name)] + public class DbConnectionManagerTests + { + public DbConnectionManagerTests(PerpetuumStaticsFixture fixture) + { + _ = fixture; + } + + [Fact] + public void Multiple_queries_inside_transaction_scope_reuse_same_connection() + { + int connectionCreatedCount = 0; + FakeDb fakeDb = new(); + + Db.DbQueryFactory = () => new DbQuery( + () => + { + connectionCreatedCount++; + return new FakeDbConnection(fakeDb); + }, + new GlobalConfiguration { DistributedTransactions = false }); + + fakeDb.When("select 1", FakeResultSet.FromRows(["x"], [1])); + fakeDb.When("select 2", FakeResultSet.FromRows(["x"], [2])); + fakeDb.When("select 3", FakeResultSet.FromRows(["x"], [3])); + + using (TransactionScope scope = Db.CreateTransaction()) + { + Db.Query("select 1").Execute(); + Db.Query("select 2").Execute(); + Db.Query("select 3").Execute(); + + Assert.Equal(1, connectionCreatedCount); + Assert.Equal(1, DbConnectionManager.ActiveConnectionCount); + + scope.Complete(); + } + + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + } + + [Fact] + public void Queries_outside_transaction_scope_use_separate_connections() + { + int connectionCreatedCount = 0; + FakeDb fakeDb = new(); + + Db.DbQueryFactory = () => new DbQuery( + () => + { + connectionCreatedCount++; + return new FakeDbConnection(fakeDb); + }, + new GlobalConfiguration { DistributedTransactions = false }); + + fakeDb.When("select 1", FakeResultSet.FromRows(["x"], [1])); + + Db.Query("select 1").Execute(); + Db.Query("select 1").Execute(); + + Assert.Equal(2, connectionCreatedCount); + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + } + + [Fact] + public void Connection_is_disposed_when_transaction_scope_aborts() + { + int connectionCreatedCount = 0; + FakeDb fakeDb = new(); + FakeDbConnection? usedConnection = null; + + Db.DbQueryFactory = () => new DbQuery( + () => + { + connectionCreatedCount++; + usedConnection = new FakeDbConnection(fakeDb); + return usedConnection; + }, + new GlobalConfiguration { DistributedTransactions = false }); + + fakeDb.When("select 1", FakeResultSet.FromRows(["x"], [1])); + + try + { + using (TransactionScope scope = Db.CreateTransaction()) + { + Db.Query("select 1").Execute(); + Assert.Equal(ConnectionState.Open, usedConnection?.State); + throw new InvalidOperationException("Simulated failure inside transaction"); + } + } + catch (InvalidOperationException) + { + // Expected + } + + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + Assert.Equal(ConnectionState.Closed, usedConnection?.State); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/DbQueryEnlistmentTests.cs b/src/Perpetuum.Tests/Unit/DbQueryEnlistmentTests.cs new file mode 100644 index 00000000..e5af3572 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/DbQueryEnlistmentTests.cs @@ -0,0 +1,113 @@ +using System.Data; +using System.Transactions; +using Perpetuum.Data; +using Perpetuum.Tests.Fakes.Data; +using Perpetuum.Tests.Infrastructure; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + /// + /// Covers the DistributedTransactions = true branch of DbQuery.ExecuteHelper, which is the + /// production Windows path (Perpetuum.Server enables it in Program.cs). Every other unit + /// test in this project runs with the flag false, so that branch was never exercised: + /// FakeDbConnection deliberately implements only IDbConnection and the + /// "connection is DbConnection" check there is always false. EnlistableFakeDbConnection is + /// the only fake that can reach it. + /// + [Collection(PerpetuumStaticsCollection.Name)] + public class DbQueryEnlistmentTests + { + public DbQueryEnlistmentTests(PerpetuumStaticsFixture fixture) + { + _ = fixture; + } + + private static FakeDb NewFakeDb() + { + FakeDb fakeDb = new(); + fakeDb.When("select 1", FakeResultSet.FromRows(["x"], [1])); + fakeDb.When("select 2", FakeResultSet.FromRows(["x"], [2])); + fakeDb.When("select 3", FakeResultSet.FromRows(["x"], [3])); + return fakeDb; + } + + [Fact] + public void With_distributed_transactions_the_shared_connection_is_enlisted_in_the_ambient_scope() + { + EnlistableFakeDbConnection? connection = null; + int connectionCreatedCount = 0; + FakeDb fakeDb = NewFakeDb(); + + Db.DbQueryFactory = () => new DbQuery( + () => + { + connectionCreatedCount++; + return connection ??= new EnlistableFakeDbConnection(fakeDb); + }, + new GlobalConfiguration { DistributedTransactions = true }); + + using (TransactionScope scope = new()) + { + Db.Query("select 1").Execute(); + Db.Query("select 2").Execute(); + Db.Query("select 3").Execute(); + + Assert.Equal(1, connectionCreatedCount); + Assert.NotNull(connection); + Assert.Equal(3, connection!.EnlistmentCount); + Assert.Same(Transaction.Current, connection.EnlistedTransaction); + Assert.Equal(1, DbConnectionManager.ActiveConnectionCount); + + scope.Complete(); + } + + Assert.Equal(0, DbConnectionManager.ActiveConnectionCount); + } + + [Fact] + public void Without_distributed_transactions_nothing_is_enlisted_even_inside_a_scope() + { + // The flag false is the Linux container path. The connection is still shared per + // scope, but it must never be handed to the transaction manager. + EnlistableFakeDbConnection? connection = null; + FakeDb fakeDb = NewFakeDb(); + + Db.DbQueryFactory = () => new DbQuery( + () => connection ??= new EnlistableFakeDbConnection(fakeDb), + new GlobalConfiguration { DistributedTransactions = false }); + + using (TransactionScope scope = new()) + { + Db.Query("select 1").Execute(); + Db.Query("select 2").Execute(); + + Assert.Equal(0, connection!.EnlistmentCount); + Assert.Null(connection.EnlistedTransaction); + scope.Complete(); + } + } + + [Fact] + public void With_distributed_transactions_queries_outside_a_scope_are_never_enlisted() + { + List connections = []; + FakeDb fakeDb = NewFakeDb(); + + Db.DbQueryFactory = () => new DbQuery( + () => + { + EnlistableFakeDbConnection connection = new(fakeDb); + connections.Add(connection); + return connection; + }, + new GlobalConfiguration { DistributedTransactions = true }); + + Db.Query("select 1").Execute(); + Db.Query("select 2").Execute(); + + Assert.Equal(2, connections.Count); + Assert.All(connections, c => Assert.Equal(0, c.EnlistmentCount)); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/DefinitionConfigSkiaTests.cs b/src/Perpetuum.Tests/Unit/DefinitionConfigSkiaTests.cs new file mode 100644 index 00000000..67e53d46 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/DefinitionConfigSkiaTests.cs @@ -0,0 +1,66 @@ +using Perpetuum.EntityFramework; +using Perpetuum.Tests.Fakes.Data; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class DefinitionConfigSkiaTests + { + private static readonly string[] ColumnNames = + [ + "definition", "targetdefinition", "npcpresenceid", "item_work_range", "explosion_radius", + "cycle_time", "damage_chemical", "damage_explosive", "damage_kinetic", "damage_thermal", + "damage_toxic", "lifetime", "activationtime", "waves", "missionrelated", + "constructionradius", "action_delay", "deploy_radius", "transmitradius", "constructionlevelmax", + "blockingradius", "chargeAmount", "inconnections", "outconnections", "coretransferred", + "transferefficiency", "productionupgradeamount", "productionlevel", "coreconsumption", + "effectid", "corecalories", "corekickstartthreshold", "reinforcecountermax", + "bandwidthusage", "bandwidthcapacity", "emitradius", "typeexclusiverange", + "network_node_range", "hitsize", "tint" + ]; + + private static DefinitionConfig CreateConfigWithTint(string? tintValue) + { + object?[] row = new object?[ColumnNames.Length]; + row[0] = 123; // definition + row[ColumnNames.Length - 1] = tintValue; // tint + + var resultSet = FakeResultSet.FromRows(ColumnNames, row); + var reader = new FakeDataReader(resultSet); + reader.Read(); + return new DefinitionConfig(reader); + } + + [Fact] + public void Tint_parses_valid_hex_color() + { + var config = CreateConfigWithTint("#FF8040"); + Assert.Equal(new SKColor(255, 128, 64, 255), config.Tint); + } + + [Fact] + public void Tint_parses_valid_hex_color_with_alpha() + { + var config = CreateConfigWithTint("#80112233"); + Assert.Equal(new SKColor(0x11, 0x22, 0x33, 0x80), config.Tint); + } + + [Fact] + public void Tint_invalid_string_results_in_default_color() + { + var config = CreateConfigWithTint("not-a-valid-color"); + Assert.Equal(default, config.Tint); + } + + [Fact] + public void Tint_null_or_empty_defaults_to_white() + { + var configNull = CreateConfigWithTint(null); + Assert.Equal(SKColors.White, configNull.Tint); + + var configEmpty = CreateConfigWithTint(string.Empty); + Assert.Equal(SKColors.White, configEmpty.Tint); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/GenxySkiaTests.cs b/src/Perpetuum.Tests/Unit/GenxySkiaTests.cs new file mode 100644 index 00000000..24e15e62 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/GenxySkiaTests.cs @@ -0,0 +1,66 @@ +using Perpetuum.GenXY; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class GenxySkiaTests + { + [Fact] + public void SKColor_serialization_and_deserialization_roundtrip() + { + var original = new SKColor(255, 128, 64, 200); + string serialized = GenxyConverter.SerializeObject(original); + + Assert.StartsWith("c", serialized); + + var deserialized = GenxyConverter.DeserializeObject(serialized); + Assert.Equal(original.Red, deserialized.Red); + Assert.Equal(original.Green, deserialized.Green); + Assert.Equal(original.Blue, deserialized.Blue); + Assert.Equal(original.Alpha, deserialized.Alpha); + } + + [Fact] + public void SKPointI_serialization_and_deserialization_roundtrip() + { + var original = new SKPointI(1234, 5678); + string serialized = GenxyConverter.SerializeObject(original); + + Assert.StartsWith("p", serialized); + + var deserialized = GenxyConverter.DeserializeObject(serialized); + Assert.Equal(original, deserialized); + } + + [Fact] + public void Area_serialization_and_deserialization_roundtrip() + { + var original = new Area(10, 20, 100, 200); + string serialized = GenxyConverter.SerializeObject(original); + + Assert.StartsWith("r", serialized); + + var deserialized = GenxyConverter.DeserializeObject(serialized); + Assert.Equal(original, deserialized); + } + + [Fact] + public void Dictionary_containing_Skia_types_roundtrips() + { + var dict = new Dictionary + { + { "tint", new SKColor(10, 20, 30, 40) }, + { "location", new SKPointI(50, 60) }, + { "boundary", new Area(1, 2, 3, 4) } + }; + + string serialized = GenxyConverter.Serialize(dict); + var deserialized = GenxyConverter.Deserialize(serialized); + + Assert.Equal(new SKColor(10, 20, 30, 40), (SKColor)deserialized["tint"]); + Assert.Equal(new SKPointI(50, 60), (SKPointI)deserialized["location"]); + Assert.Equal(new Area(1, 2, 3, 4), (Area)deserialized["boundary"]); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs b/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs new file mode 100644 index 00000000..361dabcd --- /dev/null +++ b/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs @@ -0,0 +1,70 @@ +using Perpetuum.Zones; +using Perpetuum.Zones.Terrains; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class LayerExtensionsTests + { + [Fact] + public void IsValidPosition_checks_layer_dimensions() + { + var layer = new Layer(LayerType.Altitude, 50, 60); + + Assert.True(layer.IsValidPosition(0, 0)); + Assert.True(layer.IsValidPosition(49, 59)); + Assert.False(layer.IsValidPosition(-1, 0)); + Assert.False(layer.IsValidPosition(50, 10)); + Assert.False(layer.IsValidPosition(10, 60)); + } + + [Fact] + public void GetValue_with_SKPointI_and_Position() + { + var layer = new Layer(LayerType.Altitude, 20, 20); + layer[5, 8] = 42; + + Assert.Equal(42, layer.GetValue(new SKPointI(5, 8))); + Assert.Equal(42, layer.GetValue(new Position(5.2, 8.7))); + } + + [Fact] + public void UpdateAll_and_UpdateValue() + { + var layer = new Layer(LayerType.Altitude, 10, 10); + layer.UpdateAll((x, y, _) => x + y); + + Assert.Equal(0, layer[0, 0]); + Assert.Equal(7, layer[3, 4]); + Assert.Equal(18, layer[9, 9]); + + layer.UpdateValue(3, 4, v => v * 10); + Assert.Equal(70, layer[3, 4]); + } + + [Fact] + public void GetValue_clamps_out_of_bounds() + { + var layer = new Layer(LayerType.Altitude, 10, 20); + layer[0, 0] = 100; + layer[9, 19] = 200; + + Assert.Equal(100, layer[-5, -10]); + Assert.Equal(200, layer[50, 100]); + } + + [Fact] + public void Position_IsValid_rejects_negative_fractional_coords() + { + var size = new SKSizeI(100, 100); + + Assert.True(new Position(0.0, 0.0).IsValid(size)); + Assert.True(new Position(99.9, 99.9).IsValid(size)); + Assert.False(new Position(-0.1, 50.0).IsValid(size)); + Assert.False(new Position(50.0, -0.5).IsValid(size)); + Assert.False(new Position(50.0, 50.0, -0.1).IsValid(size)); + Assert.False(new Position(100.0, 50.0).IsValid(size)); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/PathFinderSkiaTests.cs b/src/Perpetuum.Tests/Unit/PathFinderSkiaTests.cs new file mode 100644 index 00000000..13ab96ad --- /dev/null +++ b/src/Perpetuum.Tests/Unit/PathFinderSkiaTests.cs @@ -0,0 +1,101 @@ +using Perpetuum.PathFinders; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class PathFinderSkiaTests + { + [Fact] + public void AStarFinder_finds_straight_path() + { + var finder = new AStarFinder(Heuristic.Manhattan, (x, y) => x >= 0 && x < 10 && y >= 0 && y < 10); + var start = new SKPointI(0, 0); + var end = new SKPointI(5, 0); + + var path = finder.FindPath(start, end, TestContext.Current.CancellationToken); + + Assert.NotNull(path); + Assert.NotEmpty(path); + Assert.Equal(start, path.First()); + Assert.Equal(end, path.Last()); + } + + [Fact] + public void AStarFinder_navigates_around_obstacle() + { + bool Passable(int x, int y) + { + if (x < 0 || x >= 10 || y < 0 || y >= 10) return false; + if (x == 2 && y < 5) return false; + return true; + } + + var finder = new AStarFinder(Heuristic.Euclidean, Passable); + var start = new SKPointI(0, 0); + var end = new SKPointI(4, 0); + + var path = finder.FindPath(start, end, TestContext.Current.CancellationToken); + + Assert.NotNull(path); + Assert.Equal(start, path.First()); + Assert.Equal(end, path.Last()); + Assert.DoesNotContain(path, p => p.X == 2 && p.Y < 5); + } + + [Fact] + public void AStarFinder_start_equals_end_returns_empty_path() + { + var finder = new AStarFinder(Heuristic.Manhattan, (x, y) => true); + var pt = new SKPointI(3, 3); + var path = finder.FindPath(pt, pt, TestContext.Current.CancellationToken); + + Assert.NotNull(path); + Assert.Empty(path); + } + + [Fact] + public void AStarFinder_unreachable_destination_returns_null() + { + bool Passable(int x, int y) => !(x == 5 && y == 5); + + var finder = new AStarFinder(Heuristic.Manhattan, Passable); + var path = finder.FindPath(new SKPointI(0, 0), new SKPointI(5, 5), TestContext.Current.CancellationToken); + + Assert.Null(path); + } + + [Fact] + public void AStarFinder_cancellation_returns_null() + { + var finder = new AStarFinder(Heuristic.Manhattan, (x, y) => true); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var path = finder.FindPath(new SKPointI(0, 0), new SKPointI(100, 100), cts.Token); + Assert.Null(path); + } + + [Fact] + public void AStarLimited_HasPath_returns_true_for_nearby_target() + { + var limited = new AStarLimited(Heuristic.Manhattan, (x, y) => true, max: 10); + Assert.True(limited.HasPath(new SKPointI(0, 0), new SKPointI(3, 3))); + Assert.True(limited.HasPath(new SKPointI(2, 2), new SKPointI(2, 2))); + } + + [Fact] + public void AStarLimited_HasPath_returns_false_when_exceeding_max_depth() + { + var limited = new AStarLimited(Heuristic.Manhattan, (x, y) => true, max: 5); + Assert.False(limited.HasPath(new SKPointI(0, 0), new SKPointI(20, 20))); + } + + [Fact] + public void AStarLimited_HasPath_returns_false_when_destination_impassable() + { + var limited = new AStarLimited(Heuristic.Manhattan, (x, y) => !(x == 2 && y == 2), max: 10); + Assert.False(limited.HasPath(new SKPointI(0, 0), new SKPointI(2, 2))); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/PlayerUpdateLockTests.cs b/src/Perpetuum.Tests/Unit/PlayerUpdateLockTests.cs new file mode 100644 index 00000000..9ce2b800 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/PlayerUpdateLockTests.cs @@ -0,0 +1,186 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using Perpetuum; +using Perpetuum.Accounting.Characters; +using Perpetuum.EntityFramework; +using Perpetuum.Groups.Corporations; +using Perpetuum.Groups.Gangs; +using Perpetuum.Items; +using Perpetuum.Players; +using Perpetuum.Services.ExtensionService; +using Perpetuum.Services.MissionEngine; +using Perpetuum.Services.Sessions; +using Perpetuum.Tests.Fakes.Data; +using Perpetuum.Tests.Infrastructure; +using Perpetuum.Units; +using Perpetuum.Zones; +using Perpetuum.Zones.Effects.ZoneEffects; +using Perpetuum.Zones.Teleporting.Strategies; +using Perpetuum.Zones.Terrains; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + /// + /// A player becomes visible to the zone tick the moment AddToZone commits, but the entry + /// snapshot (SetSession / SendInitSelf / entry effects) still runs afterwards on the socket + /// thread (auth) or a threadpool thread (local teleport). Player.UpdateLock serializes those + /// two. This test pins that invariant by proving a tick (Unit.Update, the exact entry point + /// Zone.UpdateUnits calls) cannot complete while the entry lock is held, and completes + /// promptly once it is released. + /// + [Collection(PerpetuumStaticsCollection.Name)] + public class PlayerUpdateLockTests + { + [Fact] + public async Task A_tick_cannot_update_the_player_while_the_entry_lock_is_held() + { + // The Player constructor defaults Character to Character.None, which is produced + // by a bootstrap-time factory; unit tests have no bootstrap. + Character.CharacterFactory = _ => new Character(); + + // MissionHandler.InitMissions (called from OnEnterZone) queries the mission log. + FakeDb fakeDb = FakeDb.Install(); + fakeDb.When("SELECT * FROM dbo.missionlog", FakeResultSet.Empty("missionId")); + + TestZone zone = CreateZone(); + Player player = CreatePlayer(); + + // A production robot is loaded from the database with its definition and property + // modifiers; a bare Player has neither. The empty modifier collection falls back + // to default modifiers so the property machinery in the tick body stays NRE-free, + // and EntityDefault.None keeps Entity.Definition resolvable (the gang update + // packets built on enter/exit read it). Initialize fills the lazy module + // collections that RemoveFromZone walks. + player.ED = EntityDefault.None; + player.BasePropertyModifiers = PropertyModifierCollection.Empty; + player.Initialize(); + + // Production wires IEntityServices into every container-created entity + // (EntitiesModule: e.Instance.EntityServices = ...). The setter is protected, so the + // test injects it the same way the container does: reflectively. AddToZone runs the + // stronghold player-state save, which needs the repository. + IEntityServices entityServices = Substitute.For(); + entityServices.Repository.Returns(Substitute.For()); + typeof(Entity).GetProperty(nameof(Entity.EntityServices))!.SetValue(player, entityServices); + + try + { + player.AddToZone(zone, new Position(0, 0, 0)); + + int updateThreadId = 0; + Task updateTask = null; + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + + // The entry lock has to be held across the observation window, but + // Monitor.Enter/Exit must run on one thread while the test body is full of + // awaits (which reschedule onto the thread pool). A dedicated holder thread + // therefore owns the monitor for the duration of the window. + using SemaphoreSlim lockHeld = new(0, 1); + using SemaphoreSlim releaseLock = new(0, 1); + + Task holderTask = Task.Run(() => + { + Monitor.Enter(player.UpdateLock); + lockHeld.Release(); + releaseLock.Wait(cancellationToken); + Monitor.Exit(player.UpdateLock); + }, cancellationToken); + + await lockHeld.WaitAsync(cancellationToken); + + updateTask = Task.Run(() => + { + updateThreadId = Environment.CurrentManagedThreadId; + player.Update(TimeSpan.FromMilliseconds(50)); + }, cancellationToken); + + // Wait until the tick thread has actually started, so the assertion below + // cannot pass vacuously. + await WaitUntilAsync(() => updateThreadId != 0, TimeSpan.FromSeconds(5), + "the tick thread never started", cancellationToken); + + // The update body is in-memory and takes a small fraction of this window; + // a player update completing inside it was not blocked by the entry lock. + await Task.Delay(300, cancellationToken); + + if (updateTask.IsCompleted) + { + await updateTask; // rethrows if the update faulted + } + + Assert.False(updateTask.IsCompleted, + "Player.Update completed while the entry lock was held; OnUpdate is not serialized under UpdateLock"); + + releaseLock.Release(); + await holderTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); + + await updateTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); + } + finally + { + // Stops the movement-check thread started in OnEnterZone. + player.RemoveFromZone(); + } + } + + private static async Task WaitUntilAsync(Func condition, TimeSpan timeout, string message, CancellationToken cancellationToken) + { + using CancellationTokenSource cts = new(timeout); + using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); + while (!condition()) + { + try + { + await Task.Delay(5, linked.Token); + } + catch (TaskCanceledException) when (!cts.IsCancellationRequested) + { + throw; // test cancellation, propagate + } + catch (TaskCanceledException) + { + throw new TimeoutException(message); + } + } + } + + private static Player CreatePlayer() + { + return new Player( + Substitute.For(), + Substitute.For(), + (zone, player) => new MissionHandler(zone, player, null!), + Substitute.For(), + null!, // dockingBaseHelper: not touched on the update path + _ => null, // combatLoggerFactory: null is handled in Player + new GlobalConfiguration()); + } + + private static TestZone CreateZone() + { + TestZone zone = new TestZone(); + zone.Configuration = ZoneConfiguration.None; + zone.MiningLogHandler = new MiningLogHandler(zone); + zone.HarvestLogHandler = new HarvestLogHandler(zone); + zone.ZoneEffectHandler = Substitute.For(); + + // AddToZone -> FixZ reads the terrain altitude + ITerrain terrain = Substitute.For(); + terrain.Altitude.Returns(new AltitudeLayer(new ushort[64 * 64], 64, 64)); + zone.Terrain = terrain; + + return zone; + } + + private sealed class TestZone : Zone + { + public TestZone() + : base(Substitute.For(), Substitute.For()) + { + } + } + } +} diff --git a/src/Perpetuum.Tests/Unit/PointExtensionsTests.cs b/src/Perpetuum.Tests/Unit/PointExtensionsTests.cs new file mode 100644 index 00000000..08882f68 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/PointExtensionsTests.cs @@ -0,0 +1,174 @@ +using System.Numerics; +using Perpetuum.Zones; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class PointExtensionsTests + { + [Fact] + public void ToPosition_SKPointI_centers_at_half_tile() + { + var pt = new SKPointI(10, 25); + var pos = pt.ToPosition(); + Assert.Equal(10.5, pos.X); + Assert.Equal(25.5, pos.Y); + } + + [Fact] + public void ToPosition_SKPoint_preserves_coordinates() + { + var pt = new SKPoint(12.75f, 34.25f); + var pos = pt.ToPosition(); + Assert.Equal(12.75, pos.X, precision: 2); + Assert.Equal(34.25, pos.Y, precision: 2); + } + + [Fact] + public void ToVector2_SKPointI() + { + var pt = new SKPointI(7, 42); + var v = pt.ToVector2(); + Assert.Equal(new Vector2(7f, 42f), v); + } + + [Fact] + public void GetNonDiagonalNeighbours_returns_4_orthogonal_neighbours() + { + var pt = new SKPointI(5, 5); + var neighbours = pt.GetNonDiagonalNeighbours().ToList(); + Assert.Equal(4, neighbours.Count); + Assert.Contains(new SKPointI(5, 6), neighbours); + Assert.Contains(new SKPointI(6, 5), neighbours); + Assert.Contains(new SKPointI(5, 4), neighbours); + Assert.Contains(new SKPointI(4, 5), neighbours); + } + + [Fact] + public void GetNeighbours_returns_8_surrounding_neighbours() + { + var pt = new SKPointI(10, 10); + var neighbours = pt.GetNeighbours().ToList(); + Assert.Equal(8, neighbours.Count); + var expected = new[] + { + new SKPointI(9, 9), new SKPointI(10, 9), new SKPointI(11, 9), + new SKPointI(9, 10), new SKPointI(11, 10), + new SKPointI(9, 11), new SKPointI(10, 11), new SKPointI(11, 11) + }; + foreach (var exp in expected) + { + Assert.Contains(exp, neighbours); + } + } + + [Theory] + [InlineData(1, 9)] + [InlineData(2, 25)] + [InlineData(3, 49)] + public void GetNeighbours_with_size_returns_square_grid(int size, int expectedCount) + { + var pt = new SKPointI(10, 10); + var neighbours = pt.GetNeighbours(size).ToList(); + Assert.Equal(expectedCount, neighbours.Count); + Assert.All(neighbours, p => + { + Assert.InRange(p.X, 10 - size, 10 + size); + Assert.InRange(p.Y, 10 - size, 10 + size); + }); + } + + [Fact] + public void GetNearestPoint_finds_closest_point() + { + var origin = new SKPointI(0, 0); + var points = new[] + { + new SKPointI(10, 10), + new SKPointI(3, 4), + new SKPointI(8, 2), + new SKPointI(1, 1) + }; + + var nearest = origin.GetNearestPoint(points); + Assert.Equal(new SKPointI(1, 1), nearest); + } + + [Fact] + public void GetNearestPoint_empty_enumerable_returns_empty_point() + { + var origin = new SKPointI(5, 5); + var nearest = origin.GetNearestPoint(Array.Empty()); + Assert.Equal(SKPointI.Empty, nearest); + } + + [Theory] + [InlineData(0, 0, 3, 4, 5.0, true)] + [InlineData(0, 0, 3, 4, 4.9, false)] + [InlineData(0, 0, 0, 0, 0.0, true)] + public void IsInRange_evaluates_distance_threshold(int x1, int y1, int x2, int y2, double range, bool expected) + { + var p1 = new SKPointI(x1, y1); + var p2 = new SKPointI(x2, y2); + Assert.Equal(expected, p1.IsInRange(p2, range)); + } + + [Fact] + public void Distance_and_SqrDistance_calculations() + { + var p1 = new SKPointI(1, 2); + var p2 = new SKPointI(4, 6); + + Assert.Equal(25, p1.SqrDistance(p2)); + Assert.Equal(25, p1.SqrDistance(4, 6)); + Assert.Equal(5.0, p1.Distance(p2)); + } + + [Theory] + [InlineData(0, 0, 0, -10, 0.0)] // North + [InlineData(0, 0, 10, 0, 0.25)] // East + [InlineData(0, 0, 0, 10, 0.5)] // South + [InlineData(0, 0, -10, 0, 0.75)] // West + public void DirectionTo_cardinal_directions(int x1, int y1, int x2, int y2, double expectedDir) + { + var from = new SKPointI(x1, y1); + var to = new SKPointI(x2, y2); + double dir = from.DirectionTo(to); + Assert.Equal(expectedDir, dir, precision: 3); + } + + [Fact] + public void OffsetInDirection_roundtrip_cardinal() + { + var origin = new SKPointI(100, 100); + + var north = origin.OffsetInDirection(0.0, 10); + Assert.Equal(new SKPointI(100, 90), north); + + var south = origin.OffsetInDirection(0.5, 10); + Assert.Equal(new SKPointI(100, 110), south); + + var east = origin.OffsetInDirection(0.25, 10); + Assert.Equal(new SKPointI(110, 100), east); + + var west = origin.OffsetInDirection(0.75, 10); + Assert.Equal(new SKPointI(90, 100), west); + } + + [Fact] + public void FloodFill_bounded_by_validator() + { + var start = new SKPointI(5, 5); + var points = start.FloodFill(p => p.X >= 4 && p.X <= 6 && p.Y >= 4 && p.Y <= 6).ToList(); + + Assert.Equal(9, points.Count); + Assert.Contains(start, points); + Assert.All(points, p => + { + Assert.InRange(p.X, 4, 6); + Assert.InRange(p.Y, 4, 6); + }); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/SizeExtensionsTests.cs b/src/Perpetuum.Tests/Unit/SizeExtensionsTests.cs new file mode 100644 index 00000000..05788b0d --- /dev/null +++ b/src/Perpetuum.Tests/Unit/SizeExtensionsTests.cs @@ -0,0 +1,91 @@ +using Perpetuum.Zones; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class SizeExtensionsTests + { + [Fact] + public void Contains_SKPointI_and_xy_coordinates() + { + var size = new SKSizeI(100, 200); + + Assert.True(size.Contains(new SKPointI(0, 0))); + Assert.True(size.Contains(new SKPointI(99, 199))); + Assert.True(size.Contains(50, 100)); + + Assert.False(size.Contains(new SKPointI(-1, 0))); + Assert.False(size.Contains(new SKPointI(0, -1))); + Assert.False(size.Contains(new SKPointI(100, 100))); + Assert.False(size.Contains(new SKPointI(50, 200))); + Assert.False(size.Contains(-5, -5)); + } + + [Fact] + public void GetCenter_returns_midpoint() + { + var size = new SKSizeI(100, 60); + Assert.Equal(new SKPointI(50, 30), size.GetCenter()); + } + + [Fact] + public void ToArea_creates_matching_area() + { + var size = new SKSizeI(100, 200); + var area = size.ToArea(); + + Assert.Equal(0, area.X1); + Assert.Equal(0, area.Y1); + Assert.Equal(99, area.X2); + Assert.Equal(199, area.Y2); + Assert.Equal(100, area.Width); + Assert.Equal(200, area.Height); + } + + [Fact] + public void Ground_returns_width_times_height() + { + var size = new SKSizeI(20, 30); + Assert.Equal(600, size.Ground()); + } + + [Fact] + public void Diagonal_calculates_hypotenuse() + { + var size = new SKSizeI(3, 4); + Assert.Equal(5.0, size.Diagonal()); + } + + [Fact] + public void CreateArray_allocates_correct_length() + { + var size = new SKSizeI(10, 5); + int[] arr = size.CreateArray(); + Assert.Equal(50, arr.Length); + } + + [Fact] + public void Create2DArray_allocates_correct_dimensions() + { + var size = new SKSizeI(10, 5); + int[,] arr = size.Create2DArray(); + Assert.Equal(10, arr.GetLength(0)); + Assert.Equal(5, arr.GetLength(1)); + } + + [Fact] + public void GetRandomPosition_respects_margins() + { + var size = new SKSizeI(100, 100); + const int margin = 10; + + for (int i = 0; i < 50; i++) + { + Position p = size.GetRandomPosition(margin); + Assert.InRange(p.X, 10, 90); + Assert.InRange(p.Y, 10, 90); + } + } + } +} diff --git a/src/Perpetuum.Tests/Unit/SpatialCollectionsSkiaTests.cs b/src/Perpetuum.Tests/Unit/SpatialCollectionsSkiaTests.cs new file mode 100644 index 00000000..2f6f4f75 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/SpatialCollectionsSkiaTests.cs @@ -0,0 +1,59 @@ +using Perpetuum.Collections.Spatial; +using SkiaSharp; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class SpatialCollectionsSkiaTests + { + private class TestCell : Cell + { + public TestCell(Area area) : base(area) { } + } + + [Fact] + public void Grid_creates_cells_and_maps_coordinates() + { + var grid = new Grid(100, 100, 10, 10, area => new TestCell(area)); + + var cell1 = grid.GetCell(new SKPointI(15, 25)); + var cell2 = grid.GetCell(15, 25); + + Assert.NotNull(cell1); + Assert.Same(cell1, cell2); + Assert.Equal(10, cell1.BoundingBox.X1); + Assert.Equal(20, cell1.BoundingBox.Y1); + + Assert.Null(grid.GetCell(new SKPointI(-10, 0))); + Assert.Null(grid.GetCell(new SKPointI(105, 50))); + } + + [Fact] + public void Grid_CalculateGridSize_divides_by_TilesPerGrid() + { + var size = new SKSizeI(2048, 2048); + var gridSize = Grid.CalculateGridSize(size); + Assert.Equal(2048 / Grid.TilesPerGrid, gridSize.Width); + Assert.Equal(2048 / Grid.TilesPerGrid, gridSize.Height); + } + + [Fact] + public void QuadTree_Add_and_Query_with_SKPointI_and_Area() + { + var bounds = new Area(0, 0, 100, 100); + var quadTree = new QuadTree(bounds); + + quadTree.Add(new SKPointI(10, 10), "Item1"); + quadTree.Add(new SKPointI(20, 20), "Item2"); + quadTree.Add(new SKPointI(80, 80), "Item3"); + + var queryArea = new Area(0, 0, 30, 30); + var results = quadTree.Query(queryArea).ToList(); + + Assert.Equal(2, results.Count); + Assert.Contains(results, r => r.Value == "Item1" && r.X == 10 && r.Y == 10); + Assert.Contains(results, r => r.Value == "Item2" && r.X == 20 && r.Y == 20); + Assert.DoesNotContain(results, r => r.Value == "Item3"); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/TransactionSupportTests.cs b/src/Perpetuum.Tests/Unit/TransactionSupportTests.cs new file mode 100644 index 00000000..2872aefb --- /dev/null +++ b/src/Perpetuum.Tests/Unit/TransactionSupportTests.cs @@ -0,0 +1,39 @@ +using System.Transactions; +using Perpetuum.Data; +using Perpetuum.Tests.Infrastructure; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + /// + /// Guards the platform fallback in PerpetuumBootstrapper.Init: on Linux the raw + /// TransactionManager.ImplicitDistributedTransactions setter throws + /// PlatformNotSupportedException when enabled (observed as a server boot failure), + /// while the guarded helper must never throw. Shares the PerpetuumStaticsCollection + /// with DbQueryEnlistmentTests because both touch process-wide transaction state. + /// + [Collection(PerpetuumStaticsCollection.Name)] + public class TransactionSupportTests + { + public TransactionSupportTests(PerpetuumStaticsFixture fixture) + { + _ = fixture; + } + + [Fact] + public void Enabling_implicit_distributed_transactions_never_throws_and_restores_the_previous_state() + { + bool original = TransactionManager.ImplicitDistributedTransactions; + + try + { + TransactionSupport.SetImplicitDistributedTransactions(true); + TransactionSupport.SetImplicitDistributedTransactions(false); + } + finally + { + TransactionSupport.SetImplicitDistributedTransactions(original); + } + } + } +} diff --git a/src/Perpetuum/Area.cs b/src/Perpetuum/Area.cs index f1ebd63d..be939d44 100644 --- a/src/Perpetuum/Area.cs +++ b/src/Perpetuum/Area.cs @@ -1,7 +1,5 @@ using Perpetuum.Zones; -using System; -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum { @@ -50,7 +48,7 @@ public static Area FromRadius(Position position, int radius) return FromRadius(position.intX, position.intY, radius); } - public static Area FromRadius(Point position, int radius) + public static Area FromRadius(SKPointI position, int radius) { return FromRadius(position.X, position.Y, radius); } @@ -95,7 +93,7 @@ public Position CenterPrecise get { return new Position((_x1 + _x2) / 2.0, (_y1 + _y2) / 2.0); } } - public Point Center + public SKPointI Center { get { return new Position((Width >> 1) + _x1, (Height >> 1) + _y1); } } @@ -116,7 +114,7 @@ public int GetOffset(int x, int y) return x + y * Width; } - public bool Contains(Point target) + public bool Contains(SKPointI target) { return Contains(target.X, target.Y); } @@ -152,7 +150,7 @@ public override string ToString() return $"X1 = {X1} Y1 = {Y1} X2 = {X2} Y2 = {Y2} Width = {Width} Height = {Height}"; } - public Area Clamp(Size size) + public Area Clamp(SKSizeI size) { return Clamp(size.Width, size.Height); } @@ -207,11 +205,11 @@ private IEnumerable Slice(int w,int h) } while (y1 < _y2); } - public Point GetRandomPosition() + public SKPointI GetRandomPosition() { var x = FastRandom.NextInt(_x1,_x2); var y = FastRandom.NextInt(_y1,_y2); - return new Point(x, y); + return new SKPointI(x, y); } public Area AddBorder(int border) @@ -230,7 +228,7 @@ public IEnumerable GetPositions() } } - public double Distance(Point p) + public double Distance(SKPointI p) { return Distance(p.X,p.Y); } @@ -240,7 +238,7 @@ public double Distance(int x, int y) return Math.Sqrt(SqrDistance(x, y)); } - public double SqrDistance(Point p) + public double SqrDistance(SKPointI p) { return SqrDistance(p.X,p.Y); } diff --git a/src/Perpetuum/BinaryStream.cs b/src/Perpetuum/BinaryStream.cs index e4dc7192..4c0b633e 100644 --- a/src/Perpetuum/BinaryStream.cs +++ b/src/Perpetuum/BinaryStream.cs @@ -1,7 +1,7 @@ using Perpetuum.Zones; using System.Diagnostics; -using System.Drawing; using System.Text; +using SkiaSharp; namespace Perpetuum { @@ -98,11 +98,11 @@ public void AppendObject(object o) return; } - if (o is Color color) + if (o is SKColor color) { - AppendByte(color.R); - AppendByte(color.G); - AppendByte(color.B); + AppendByte(color.Red); + AppendByte(color.Green); + AppendByte(color.Blue); return; } @@ -168,7 +168,7 @@ public void AppendStream(BinaryStream stream) AppendByteArray(stream.ToArray()); } - public void AppendPoint(Point p) + public void AppendPoint(SKPointI p) { AppendInt(p.X); AppendInt(p.Y); diff --git a/src/Perpetuum/BitmapExtensions.cs b/src/Perpetuum/BitmapExtensions.cs index 7a1ae46c..baf5c6a6 100644 --- a/src/Perpetuum/BitmapExtensions.cs +++ b/src/Perpetuum/BitmapExtensions.cs @@ -1,20 +1,20 @@ -using System.Drawing; +using SkiaSharp; namespace Perpetuum { public static class BitmapExtensions { [CanBeNull] - public static Bitmap? WithGraphics(this Bitmap bitmap, Action action) + public static SKBitmap? WithCanvas(this SKBitmap bitmap, Action action) { if (bitmap == null) { return null; } - using (Graphics g = Graphics.FromImage(bitmap)) + using (var canvas = new SKCanvas(bitmap)) { - action(g); + action(canvas); } return bitmap; @@ -24,7 +24,7 @@ public static class BitmapExtensions /// Runs an action on every pixel of a bitmap /// [CanBeNull] - public static Bitmap? ForEach(this Bitmap bitmap, Action action) + public static SKBitmap? ForEach(this SKBitmap bitmap, Action action) { if (bitmap == null) { diff --git a/src/Perpetuum/Collections/Spatial/Grid.NonGeneric.cs b/src/Perpetuum/Collections/Spatial/Grid.NonGeneric.cs index 6d341261..62e49390 100644 --- a/src/Perpetuum/Collections/Spatial/Grid.NonGeneric.cs +++ b/src/Perpetuum/Collections/Spatial/Grid.NonGeneric.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Collections.Spatial { @@ -20,9 +19,9 @@ public class Grid {GridDistricts.RightLower,new CellCoord(1, 1)} }; - public static Size CalculateGridSize(Size size) + public static SKSizeI CalculateGridSize(SKSizeI size) { - return new Size(size.Width / TilesPerGrid, size.Height / TilesPerGrid); + return new SKSizeI(size.Width / TilesPerGrid, size.Height / TilesPerGrid); } } } \ No newline at end of file diff --git a/src/Perpetuum/Collections/Spatial/Grid.cs b/src/Perpetuum/Collections/Spatial/Grid.cs index 51838d9c..b2b01497 100644 --- a/src/Perpetuum/Collections/Spatial/Grid.cs +++ b/src/Perpetuum/Collections/Spatial/Grid.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Collections.Spatial { @@ -48,8 +46,8 @@ public Grid(int width, int height, int cellsX, int cellsY, Func cel neighbours[cell] = new List(); - Point p = new Point(x, y); - foreach (Point np in p.GetNeighbours()) + SKPointI p = new SKPointI(x, y); + foreach (SKPointI np in p.GetNeighbours()) { if (np.X < 0 || np.X >= numCellsX || np.Y < 0 || np.Y >= numCellsY) { @@ -68,7 +66,7 @@ private int GetCellCoordIndex(int x, int y) } [CanBeNull] - public TCell GetCell(Point p) + public TCell GetCell(SKPointI p) { return GetCell(p.X, p.Y); } diff --git a/src/Perpetuum/Collections/Spatial/QuadTree.cs b/src/Perpetuum/Collections/Spatial/QuadTree.cs index 5c4aa414..7db1eb21 100644 --- a/src/Perpetuum/Collections/Spatial/QuadTree.cs +++ b/src/Perpetuum/Collections/Spatial/QuadTree.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Collections.Spatial { @@ -12,7 +11,7 @@ public QuadTree(Area area) Root = new QuadTreeNode(area); } - public QuadTreeItem Add(Point position, T value) + public QuadTreeItem Add(SKPointI position, T value) { return Add(position.X, position.Y, value); } diff --git a/src/Perpetuum/ColorExtensions.cs b/src/Perpetuum/ColorExtensions.cs new file mode 100644 index 00000000..e5aeddfe --- /dev/null +++ b/src/Perpetuum/ColorExtensions.cs @@ -0,0 +1,17 @@ +using SkiaSharp; + +namespace Perpetuum +{ + public static class ColorExtensions + { + /// + /// Get the luminance/brightness of this SKColor. + /// + /// The color/pixel to evaluate + /// Luminance between 0.0 and 1.0 + public static float GetLuminance(this SKColor color) + { + return (0.299f * color.Red + 0.587f * color.Green + 0.114f * color.Blue) / 255f; + } + } +} \ No newline at end of file diff --git a/src/Perpetuum/Commands.cs b/src/Perpetuum/Commands.cs index f7e02dea..3a2229f6 100644 --- a/src/Perpetuum/Commands.cs +++ b/src/Perpetuum/Commands.cs @@ -1,5 +1,5 @@ -using System.Drawing; using System.Reflection; +using SkiaSharp; namespace Perpetuum { @@ -4825,7 +4825,7 @@ public static Command GetCommandByText(string commandText) Arguments = { new Argument(k.robotEID), - new Argument(k.tint), + new Argument(k.tint), } }; diff --git a/src/Perpetuum/Data/ConnectionStringSupport.cs b/src/Perpetuum/Data/ConnectionStringSupport.cs index d88911f7..192d65a4 100644 --- a/src/Perpetuum/Data/ConnectionStringSupport.cs +++ b/src/Perpetuum/Data/ConnectionStringSupport.cs @@ -21,7 +21,7 @@ public static class ConnectionStringSupport /// list when the string is usable, absent, or malformed beyond this check — in the last /// case the driver's own error is the better explanation and is left to stand. /// - public static IReadOnlyList FindUnsupportedKeywords(string connectionString) + public static IReadOnlyList FindUnsupportedKeywords(string? connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) { diff --git a/src/Perpetuum/Data/DbConnectionManager.cs b/src/Perpetuum/Data/DbConnectionManager.cs new file mode 100644 index 00000000..9ec96f71 --- /dev/null +++ b/src/Perpetuum/Data/DbConnectionManager.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Transactions; +using Perpetuum.Log; + +namespace Perpetuum.Data +{ + /// + /// Manages sharing of open database connections within ambient transaction scopes. + /// Reusing a single open connection across multiple queries inside the same TransactionScope + /// ensures local transaction isolation (LTM) on SQL Server without triggering distributed + /// transaction escalation (MSDTC), ensuring full compatibility with Linux and reducing + /// connection pool contention. + /// + public static class DbConnectionManager + { + private static readonly ConcurrentDictionary ActiveConnections = new(); + private static readonly object SyncRoot = new(); + + public static IDbConnection GetOrCreateConnection(Transaction transaction, DbConnectionFactory connectionFactory) + { + if (ActiveConnections.TryGetValue(transaction, out IDbConnection? existingConn)) + { + return existingConn; + } + + lock (SyncRoot) + { + if (ActiveConnections.TryGetValue(transaction, out existingConn)) + { + return existingConn; + } + + IDbConnection connection = connectionFactory(); + connection.Open(); + + ActiveConnections[transaction] = connection; + + transaction.TransactionCompleted += (sender, e) => + { + lock (SyncRoot) + { + ActiveConnections.TryRemove(transaction, out _); + } + + try + { + connection.Dispose(); + } + catch (Exception ex) + { + Logger.Exception(ex); + } + }; + + return connection; + } + } + + public static int ActiveConnectionCount => ActiveConnections.Count; + } +} diff --git a/src/Perpetuum/Data/DbQuery.cs b/src/Perpetuum/Data/DbQuery.cs index ef8edaa1..0614cfbb 100644 --- a/src/Perpetuum/Data/DbQuery.cs +++ b/src/Perpetuum/Data/DbQuery.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Data.Common; using System.Transactions; @@ -6,7 +6,7 @@ namespace Perpetuum.Data { public delegate IDbConnection DbConnectionFactory(); - public class DbQuery(DbConnectionFactory connectionFactory) + public class DbQuery(DbConnectionFactory connectionFactory, GlobalConfiguration configuration) { private readonly DbConnectionFactory _connectionFactory = connectionFactory; @@ -51,33 +51,55 @@ public DbQuery SetParameter(string name, object value) private T ExecuteHelper(Func execute) { - using IDbConnection connection = _connectionFactory(); - connection.Open(); + Transaction? currentTx = Transaction.Current; + IDbConnection connection; + bool shouldDisposeConnection = true; - if (Transaction.Current != null && connection is DbConnection dbConnection) + if (currentTx != null) { - dbConnection.EnlistTransaction(Transaction.Current); + connection = DbConnectionManager.GetOrCreateConnection(currentTx, _connectionFactory); + shouldDisposeConnection = false; + } + else + { + connection = _connectionFactory(); + connection.Open(); } - IDbCommand command = connection.CreateCommand(); - command.CommandText = _commandText; - command.CommandType = _commandText.Contains(' ') ? CommandType.Text : CommandType.StoredProcedure; - command.CommandTimeout = _commandTimeout; - - if (_parameters != null) + try { - foreach (KeyValuePair kvp in _parameters) + if (configuration.DistributedTransactions && currentTx != null && connection is DbConnection dbConnection) { - IDbDataParameter parameter = command.CreateParameter(); - parameter.ParameterName = kvp.Key; - parameter.Value = kvp.Value ?? DBNull.Value; - command.Parameters.Add(parameter); + dbConnection.EnlistTransaction(currentTx); } - } - using (command) + IDbCommand command = connection.CreateCommand(); + command.CommandText = _commandText; + command.CommandType = _commandText.Contains(' ') ? CommandType.Text : CommandType.StoredProcedure; + command.CommandTimeout = _commandTimeout; + + if (_parameters != null) + { + foreach (KeyValuePair kvp in _parameters) + { + IDbDataParameter parameter = command.CreateParameter(); + parameter.ParameterName = kvp.Key; + parameter.Value = kvp.Value ?? DBNull.Value; + command.Parameters.Add(parameter); + } + } + + using (command) + { + return execute(command); + } + } + finally { - return execute(command); + if (shouldDisposeConnection) + { + connection.Dispose(); + } } } diff --git a/src/Perpetuum/Data/TransactionSupport.cs b/src/Perpetuum/Data/TransactionSupport.cs new file mode 100644 index 00000000..ed8ebb79 --- /dev/null +++ b/src/Perpetuum/Data/TransactionSupport.cs @@ -0,0 +1,41 @@ +using System.Transactions; +using Perpetuum.Log; + +namespace Perpetuum.Data +{ + /// + /// Platform-aware, best-effort switch for implicit distributed transactions; it never throws. + /// On Windows, enabling TransactionManager.ImplicitDistributedTransactions makes new scopes + /// distributed at creation. The runtime pins the value once it is set or once distributed + /// transactions have been initialized (further changes throw InvalidOperationException), + /// and on platforms without a DTC coordinator (e.g. Linux) the setter throws + /// PlatformNotSupportedException when enabled. In both cases the current value is kept and + /// scopes run as local transactions. The explicit EnlistTransaction in DbQuery still provides + /// commit/rollback semantics for single-resource scopes (see DistributedTransactionTests, + /// which covers that path against a live database). + /// + public static class TransactionSupport + { + public static void SetImplicitDistributedTransactions(bool enabled) + { + try + { + TransactionManager.ImplicitDistributedTransactions = enabled; + } + catch (PlatformNotSupportedException) + { + Logger.Warning( + "DistributedTransactions is enabled, but this platform does not support implicit distributed transactions. " + + "TransactionScopes will run as local transactions with explicit enlistment."); + } + catch (InvalidOperationException) + { + // The value is already pinned by the runtime (set earlier, or distributed + // transactions already initialized); the current behaviour stands. + Logger.Warning( + "DistributedTransactions could not be set to " + enabled + + "; the runtime has already fixed the value. TransactionScopes keep their current behaviour."); + } + } + } +} diff --git a/src/Perpetuum/EntityFramework/DefinitionConfig.cs b/src/Perpetuum/EntityFramework/DefinitionConfig.cs index bfce9500..b28b5ba6 100644 --- a/src/Perpetuum/EntityFramework/DefinitionConfig.cs +++ b/src/Perpetuum/EntityFramework/DefinitionConfig.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Drawing; +using System.Data; using Perpetuum.Data; using Perpetuum.Log; +using SkiaSharp; namespace Perpetuum.EntityFramework { @@ -51,7 +49,7 @@ public class DefinitionConfig private readonly double _hitSize; - private readonly Color _tint = Color.White; + private readonly SKColor _tint = SKColors.White; private readonly double? _coreCalories; @@ -88,7 +86,7 @@ public int ConstructionRadius get { return (int) constructionRadius.ThrowIfNull(ErrorCodes.ServerError); } } - public Color Tint + public SKColor Tint { get { return _tint; } } @@ -146,7 +144,11 @@ public DefinitionConfig(IDataRecord record) if (!string.IsNullOrEmpty(tint)) { - _tint = ColorTranslator.FromHtml(tint); + bool success = SKColor.TryParse(tint, out _tint); + if (!success) + { + Logger.Info($"Could not parse tint {_tint}"); + } } } diff --git a/src/Perpetuum/EnumerableExtensions.cs b/src/Perpetuum/EnumerableExtensions.cs index f0d9f83c..a6271ed0 100644 --- a/src/Perpetuum/EnumerableExtensions.cs +++ b/src/Perpetuum/EnumerableExtensions.cs @@ -1,10 +1,7 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; namespace Perpetuum { diff --git a/src/Perpetuum/GenXY/GenxyConverter.cs b/src/Perpetuum/GenXY/GenxyConverter.cs index a34a570e..70084960 100644 --- a/src/Perpetuum/GenXY/GenxyConverter.cs +++ b/src/Perpetuum/GenXY/GenxyConverter.cs @@ -1,8 +1,8 @@ using Perpetuum.Zones; using System.Collections; using System.Diagnostics; -using System.Drawing; using System.Dynamic; +using SkiaSharp; namespace Perpetuum.GenXY { @@ -33,9 +33,9 @@ static GenxyConverter() RegisterConverter(ConvertDecimalArray); RegisterConverter(ConvertFloat); RegisterConverter(CreateDoubleConverter); - RegisterConverter(ConvertColor); + RegisterConverter(ConvertColor); RegisterConverter(ConvertDateTime); - RegisterConverter(ConvertPoint); + RegisterConverter(ConvertPoint); RegisterConverter(ConvertPosition); RegisterConverter(ConvertPositionArray); RegisterConverter(ConvertArea); @@ -127,16 +127,16 @@ private static void CreateDoubleConverter(GenxyWriter writer, double value) ConvertFloat(writer, (float)value); } - private static void ConvertColor(GenxyWriter writer, Color color) + private static void ConvertColor(GenxyWriter writer, SKColor color) { writer.WriteToken(GenxyToken.Color); - writer.WriteHexInteger(color.R); + writer.WriteHexInteger(color.Red); writer.WriteChar('.'); - writer.WriteHexInteger(color.G); + writer.WriteHexInteger(color.Green); writer.WriteChar('.'); - writer.WriteHexInteger(color.B); + writer.WriteHexInteger(color.Blue); writer.WriteChar('.'); - writer.WriteHexInteger(color.A); + writer.WriteHexInteger(color.Alpha); } private static void ConvertDateTime(GenxyWriter writer, DateTime date) @@ -155,7 +155,7 @@ private static void ConvertDateTime(GenxyWriter writer, DateTime date) writer.WriteInteger(date.Second); } - private static void ConvertPoint(GenxyWriter writer, Point point) + private static void ConvertPoint(GenxyWriter writer, SKPointI point) { writer.WriteToken(GenxyToken.Point); writer.WriteHexInteger(point.X); diff --git a/src/Perpetuum/GenXY/GenxyReader.cs b/src/Perpetuum/GenXY/GenxyReader.cs index f22f85d8..dcab0b96 100644 --- a/src/Perpetuum/GenXY/GenxyReader.cs +++ b/src/Perpetuum/GenXY/GenxyReader.cs @@ -1,8 +1,8 @@ using Perpetuum.Threading; using Perpetuum.Zones; -using System.Drawing; using System.Globalization; using System.Text; +using SkiaSharp; namespace Perpetuum.GenXY { @@ -245,10 +245,10 @@ private DateTime ReadDate() return new DateTime(n[0], n[1], n[2], n[3], n[4], n[5]); } - private Color ReadColor() + private SKColor ReadColor() { int[] n = ReadValueAsArray(ParseInt, '.'); - return Color.FromArgb(n[3], n[0], n[1], n[2]); + return new SKColor((byte)n[0], (byte)n[1], (byte)n[2], (byte)n[3]); } private Area ReadArea() @@ -262,10 +262,10 @@ private Area[] ReadAreaArray() return ReadValueAsArray(ParseArea); } - private Point ReadPoint() + private SKPointI ReadPoint() { int[] n = ReadValueAsArray(ParseInt, '.'); - return new Point(n[0], n[1]); + return new SKPointI(n[0], n[1]); } private Position ReadPosition() diff --git a/src/Perpetuum/GlobalConfiguration.cs b/src/Perpetuum/GlobalConfiguration.cs index 99ae45f5..b1651863 100644 --- a/src/Perpetuum/GlobalConfiguration.cs +++ b/src/Perpetuum/GlobalConfiguration.cs @@ -25,6 +25,8 @@ public class GlobalConfiguration public bool EnableDev { get; set; } + public bool DistributedTransactions { get; set; } + public CorporationConfiguration Corporation { get; set; } public bool StartServerInAdminOnlyMode { get; set; } diff --git a/src/Perpetuum/Items/Paint.cs b/src/Perpetuum/Items/Paint.cs index 9bfa9dce..34e3a1fe 100644 --- a/src/Perpetuum/Items/Paint.cs +++ b/src/Perpetuum/Items/Paint.cs @@ -1,11 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using Perpetuum.Accounting.Characters; -using Perpetuum.Common.Loggers.Transaction; -using Perpetuum.Containers; -using Perpetuum.Data; -using Perpetuum.EntityFramework; +using Perpetuum.Accounting.Characters; using Perpetuum.Robots; namespace Perpetuum.Items diff --git a/src/Perpetuum/Modules/DrillerModule.cs b/src/Perpetuum/Modules/DrillerModule.cs index 4d9818c7..2f10f631 100644 --- a/src/Perpetuum/Modules/DrillerModule.cs +++ b/src/Perpetuum/Modules/DrillerModule.cs @@ -15,8 +15,8 @@ using Perpetuum.Zones.Terrains.Materials; using Perpetuum.Zones.Terrains.Materials.Minerals; using System.Diagnostics; -using System.Drawing; using System.Transactions; +using SkiaSharp; namespace Perpetuum.Modules { @@ -69,7 +69,7 @@ public override void UpdateProperty(AggregateField field) base.UpdateProperty(field); } - public List Extract(MineralLayer layer, Point location, uint amount) + public List Extract(MineralLayer layer, SKPointI location, uint amount) { if (!layer.HasMineral(location)) { diff --git a/src/Perpetuum/Modules/LargeHarvesterModule.cs b/src/Perpetuum/Modules/LargeHarvesterModule.cs index 12dcf29e..30e5f34e 100644 --- a/src/Perpetuum/Modules/LargeHarvesterModule.cs +++ b/src/Perpetuum/Modules/LargeHarvesterModule.cs @@ -61,7 +61,7 @@ public override void DoHarvesting(IZone zone) Debug.Assert(ParentRobot != null, "ParentRobot != null"); - Robots.RobotInventory container = ParentRobot.GetContainer(); + Robots.RobotInventory container = ParentRobot.GetContainer(); Debug.Assert(container != null, "container != null"); container.EnlistTransaction(); Player player = ParentRobot is RemoteControlledCreature remoteControlledCreature && diff --git a/src/Perpetuum/Network/SocketExtensions.cs b/src/Perpetuum/Network/SocketExtensions.cs index 5492835d..864ade74 100644 --- a/src/Perpetuum/Network/SocketExtensions.cs +++ b/src/Perpetuum/Network/SocketExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Sockets; +using System.Net.Sockets; using Perpetuum.Log; namespace Perpetuum.Network @@ -9,7 +8,19 @@ public static class SocketExtensions [UsedImplicitly] public static void SetKeepAlive(this Socket socket, bool state, TimeSpan time, TimeSpan interval) { - socket.SetKeepAlive(state, (uint)time.TotalMilliseconds, (uint)interval.TotalMilliseconds); + double keepAliveTime, keepAliveInterval; + if (OperatingSystem.IsWindows()) + { + keepAliveTime = time.TotalMilliseconds; + keepAliveInterval = interval.TotalMilliseconds; + socket.SetKeepAlive(state, (uint)keepAliveTime, (uint)keepAliveInterval); + } + // else + // { + // // Disabled: Not supported by linux + // keepAliveTime = time.Seconds; + // keepAliveInterval = interval.Seconds; + // } } public static void SetKeepAlive(this Socket socket, bool state, uint time, uint interval) diff --git a/src/Perpetuum/Network/TcpConnection.cs b/src/Perpetuum/Network/TcpConnection.cs index 8eca9e4b..e549c29b 100644 --- a/src/Perpetuum/Network/TcpConnection.cs +++ b/src/Perpetuum/Network/TcpConnection.cs @@ -31,7 +31,9 @@ public TcpConnection(Socket socket) _socket.NoDelay = true; _socket.ReceiveBufferSize = RECEIVE_BUFFER_SIZE; _socket.SendBufferSize = SEND_BUFFER_SIZE; - _socket.SetKeepAlive(true, 1000 * 60 * 60 * 2, 5000); + var keepAliveTime = new TimeSpan(2, 0, 0); // 2 hours + var keepAliveInterval = new TimeSpan(0, 0, 5); // 5 seconds + _socket.SetKeepAlive(true, keepAliveTime, keepAliveInterval); _activity = new ConnectionActivity(DateTime.Now); @@ -372,4 +374,4 @@ private void OnHandleSocketException(SocketException soex) } } } -} \ No newline at end of file +} diff --git a/src/Perpetuum/PathFinders/AStarFinder.cs b/src/Perpetuum/PathFinders/AStarFinder.cs index 729e0786..a2516f13 100644 --- a/src/Perpetuum/PathFinders/AStarFinder.cs +++ b/src/Perpetuum/PathFinders/AStarFinder.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Threading; -using Perpetuum.Collections; -using Perpetuum.ExportedTypes; -using Perpetuum.Zones; +using Perpetuum.Collections; +using SkiaSharp; namespace Perpetuum.PathFinders { @@ -27,7 +22,7 @@ public AStarLimited(Heuristic heuristic, PathFinderNodePassableHandler passableH /// Start point /// End point /// True if path is found and shorter than MAX_DEPTH - public bool HasPath(Point start, Point end) + public bool HasPath(SKPointI start, SKPointI end) { if (!_passableHandler(end.X, end.Y)) return false; @@ -92,7 +87,7 @@ public AStarFinder(Heuristic heuristic,PathFinderNodePassableHandler passableHan public int Weight { get; set; } - public override Point[] FindPath(Point start, Point end,CancellationToken cancellationToken) + public override SKPointI[] FindPath(SKPointI start, SKPointI end, CancellationToken cancellationToken) { if (!_passableHandler(end.X, end.Y)) return null; @@ -138,9 +133,9 @@ public override Point[] FindPath(Point start, Point end,CancellationToken cancel return null; } - protected Point[] Backtrace(Node node) + protected SKPointI[] Backtrace(Node node) { - var stack = new Stack(); + var stack = new Stack(); while (node != null) { diff --git a/src/Perpetuum/PathFinders/PathFinder.cs b/src/Perpetuum/PathFinders/PathFinder.cs index 888c0009..34d16874 100644 --- a/src/Perpetuum/PathFinders/PathFinder.cs +++ b/src/Perpetuum/PathFinders/PathFinder.cs @@ -1,17 +1,15 @@ using System.Diagnostics; -using System.Drawing; -using System.Threading; -using System.Threading.Tasks; +using SkiaSharp; namespace Perpetuum.PathFinders { public class PathFinderNode { - public Point Location { get; private set; } + public SKPointI Location { get; private set; } public PathFinderNode(int x,int y) { - Location = new Point(x,y); + Location = new SKPointI(x,y); } public override string ToString() @@ -29,7 +27,7 @@ public abstract class PathFinder { public const float SQRT2 = 1.41f; - protected static readonly Point[] EmptyPath = new Point[0]; + protected static readonly SKPointI[] EmptyPath = []; public delegate bool PathFinderNodePassableHandler(int x, int y); @@ -40,18 +38,18 @@ public abstract class PathFinder #endif [CanBeNull] - public Point[] FindPath(Point start, Point end) + public SKPointI[] FindPath(SKPointI start, SKPointI end) { return FindPath(start, end, CancellationToken.None); } - public Task FindPathAsync(Point start, Point end) + public Task FindPathAsync(SKPointI start, SKPointI end) { return Task.Run(() => FindPath(start, end)); } [CanBeNull] - public abstract Point[] FindPath(Point start, Point end, CancellationToken cancellationToken); + public abstract SKPointI[] FindPath(SKPointI start, SKPointI end, CancellationToken cancellationToken); [Conditional("DEBUG")] public void RegisterDebugHandler(PathFinderDebugHandler handler) diff --git a/src/Perpetuum/Perpetuum.csproj b/src/Perpetuum/Perpetuum.csproj index 1f41f0fc..4c69af86 100644 --- a/src/Perpetuum/Perpetuum.csproj +++ b/src/Perpetuum/Perpetuum.csproj @@ -23,7 +23,8 @@ - + + diff --git a/src/Perpetuum/Players/Player.cs b/src/Perpetuum/Players/Player.cs index d83bd721..2aa5fb43 100644 --- a/src/Perpetuum/Players/Player.cs +++ b/src/Perpetuum/Players/Player.cs @@ -57,6 +57,7 @@ public sealed class Player : Robot, IBlobableUnit, IBlobEmitter private readonly IBlobEmitter blobEmitter; private readonly BlobHandler blobHandler; private readonly PlayerMovement movement; + private readonly object _updateLock = new(); private readonly IntervalTimer combatTimer = new IntervalTimer(TimeSpan.FromSeconds(10)); private readonly GlobalConfiguration globalConfiguration; private CombatLogger combatLogger; @@ -958,15 +959,25 @@ protected override void OnRemovedFromZone(IZone zone) base.OnRemovedFromZone(zone); } + /// + /// Serializes the zone-entry snapshot (SetSession / SendInitSelf / entry effects, which run + /// on the socket or threadpool thread) with this player's tick update, so the snapshot is + /// never taken from a player that is mid-. + /// + public object UpdateLock => _updateLock; + protected override void OnUpdate(TimeSpan time) { - base.OnUpdate(time); - UpdateCombat(time); - movement.Update(time); - blobHandler.Update(time); - MissionHandler.Update(time); - combatLogger?.Update(time); - despawnHelper?.Update(time, this); + lock (_updateLock) + { + base.OnUpdate(time); + UpdateCombat(time); + movement.Update(time); + blobHandler.Update(time); + MissionHandler.Update(time); + combatLogger?.Update(time); + despawnHelper?.Update(time, this); + } } private void OnUnitUpdated(Unit unit, UnitUpdatedEventArgs e) diff --git a/src/Perpetuum/Players/PlayerMoveChecker.cs b/src/Perpetuum/Players/PlayerMoveChecker.cs index cbe8a64a..c6998dbe 100644 --- a/src/Perpetuum/Players/PlayerMoveChecker.cs +++ b/src/Perpetuum/Players/PlayerMoveChecker.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Concurrent; using Perpetuum.Log; using Perpetuum.PathFinders; using Perpetuum.Threading; diff --git a/src/Perpetuum/PointExtensions.cs b/src/Perpetuum/PointExtensions.cs index 13e12bde..48889b05 100644 --- a/src/Perpetuum/PointExtensions.cs +++ b/src/Perpetuum/PointExtensions.cs @@ -1,6 +1,6 @@ using Perpetuum.Zones; -using System.Drawing; using System.Numerics; +using SkiaSharp; namespace Perpetuum { @@ -9,35 +9,35 @@ public static class PointExtensions private static readonly int[,] _neighbours = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { -1, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 } }; private static readonly int[,] _nonDiagonalNeighbours = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } }; - public static Position ToPosition(this Point p) + public static Position ToPosition(this SKPointI p) { return new Position(p.X + 0.5, p.Y + 0.5); } - public static Position ToPosition(this PointF p) + public static Position ToPosition(this SKPoint p) { return new Position(p.X, p.Y); } - public static IEnumerable GetNonDiagonalNeighbours(this Point point) + public static IEnumerable GetNonDiagonalNeighbours(this SKPointI point) { for (int i = 0; i < 4; i++) { int nx = point.X + _nonDiagonalNeighbours[i, 0]; int ny = point.Y + _nonDiagonalNeighbours[i, 1]; - yield return new Point(nx, ny); + yield return new SKPointI(nx, ny); } } - public static IEnumerable GetNeighbours(this Point point) + public static IEnumerable GetNeighbours(this SKPointI point) { for (int i = 0; i < 8; i++) { int nx = point.X + _neighbours[i, 0]; int ny = point.Y + _neighbours[i, 1]; - yield return new Point(nx, ny); + yield return new SKPointI(nx, ny); } } @@ -53,7 +53,7 @@ public static IEnumerable GetNeighbours(this Vector2 v) } - public static IEnumerable GetNeighbours(this Point point, int size) + public static IEnumerable GetNeighbours(this SKPointI point, int size) { for (int y = -size; y <= size; y++) { @@ -62,7 +62,7 @@ public static IEnumerable GetNeighbours(this Point point, int size) int nx = point.X + x; int ny = point.Y + y; - yield return new Point(nx, ny); + yield return new SKPointI(nx, ny); } } } @@ -81,12 +81,12 @@ public static IEnumerable GetNeighbours(this Vector2 v, int size) } } - public static Point GetNearestPoint(this Point point, IEnumerable points) + public static SKPointI GetNearestPoint(this SKPointI point, IEnumerable points) { - Point nearestPoint = Point.Empty; + SKPointI nearestPoint = SKPointI.Empty; int nearestDistSq = int.MaxValue; - foreach (Point p in points) + foreach (SKPointI p in points) { int distSqr = SqrDistance(point, p); if (distSqr >= nearestDistSq) @@ -101,22 +101,22 @@ public static Point GetNearestPoint(this Point point, IEnumerable points) return nearestPoint; } - public static bool IsInRange(this Point p1, Point p2, double range) + public static bool IsInRange(this SKPointI p1, SKPointI p2, double range) { return p1.SqrDistance(p2) <= range * range; } - public static double Distance(this Point p1, Point p2) + public static double Distance(this SKPointI p1, SKPointI p2) { return Math.Sqrt(SqrDistance(p1, p2)); } - public static int SqrDistance(this Point p1, Point p2) + public static int SqrDistance(this SKPointI p1, SKPointI p2) { return SqrDistance(p1, p2.X, p2.Y); } - public static int SqrDistance(this Point p1, int x, int y) + public static int SqrDistance(this SKPointI p1, int x, int y) { int dx = p1.X - x; int dy = p1.Y - y; @@ -124,7 +124,7 @@ public static int SqrDistance(this Point p1, int x, int y) } [UsedImplicitly] - public static double DirectionTo(this Point from, Point to) + public static double DirectionTo(this SKPointI from, SKPointI to) { int dx = to.X - from.X; int dy = to.Y - from.Y; @@ -157,29 +157,29 @@ public static double DirectionTo(this Point from, Point to) private const double PI2 = Math.PI * 2; - public static Point OffsetInDirection(this Point p, double direction, double distance) + public static SKPointI OffsetInDirection(this SKPointI p, double direction, double distance) { double angleRadians = direction * PI2; double deltaX = Math.Sin(angleRadians) * distance; double deltaY = Math.Cos(angleRadians) * distance; - return new Point((int)(p.X + deltaX), (int)(p.Y - deltaY)); + return new SKPointI((int)(p.X + deltaX), (int)(p.Y - deltaY)); } - public static IEnumerable FloodFill(this Point p, Func? validator = null) + public static IEnumerable FloodFill(this SKPointI p, Func? validator = null) { - Queue q = new(); + Queue q = new(); q.Enqueue(p); - HashSet closed = new() + HashSet closed = new() { p }; - while (q.TryDequeue(out Point current)) + while (q.TryDequeue(out SKPointI current)) { yield return current; - foreach (Point np in current.GetNeighbours()) + foreach (SKPointI np in current.GetNeighbours()) { if (closed.Contains(np)) { @@ -198,7 +198,7 @@ public static IEnumerable FloodFill(this Point p, Func? vali } } - public static Vector2 ToVector2(this Point p) + public static Vector2 ToVector2(this SKPointI p) { return new Vector2(p.X, p.Y); } diff --git a/src/Perpetuum/Robots/Robot.Properties.cs b/src/Perpetuum/Robots/Robot.Properties.cs index 62bb25aa..f1b2ce4d 100644 --- a/src/Perpetuum/Robots/Robot.Properties.cs +++ b/src/Perpetuum/Robots/Robot.Properties.cs @@ -2,14 +2,14 @@ using Perpetuum.Items; using Perpetuum.Modules; using Perpetuum.Units; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Robots { public partial class Robot { private UnitOptionalProperty decay; - private UnitOptionalProperty tint; + private UnitOptionalProperty tint; private ItemProperty powerGridMax; private ItemProperty powerGrid; @@ -30,7 +30,7 @@ private void InitProperties() }; OptionalProperties.Add(decay); - tint = new UnitOptionalProperty(this, UnitDataType.Tint, k.tint, () => ED.Config.Tint); + tint = new UnitOptionalProperty(this, UnitDataType.Tint, k.tint, () => ED.Config.Tint); OptionalProperties.Add(tint); powerGridMax = new UnitProperty(this, AggregateField.powergrid_max, AggregateField.powergrid_max_modifier); @@ -85,7 +85,7 @@ public int Decay set => decay.Value = value & 255; } - public Color Tint + public SKColor Tint { get => tint.Value; set => tint.Value = value; @@ -215,7 +215,7 @@ protected override double CalculateValue() protected virtual double CamouflageBonus() { // Average value of color components. - double average = (Tint.R + Tint.G + Tint.B) / 3.0; + double average = (Tint.Red + Tint.Green + Tint.Blue) / 3.0; if (Zone == null) { return 0; @@ -224,11 +224,11 @@ protected virtual double CamouflageBonus() var oneColor = Zone.Configuration.RaceId switch { // Pelistal - 1 => Tint.G, + 1 => Tint.Green, // Nuimqol - 2 => Tint.B, + 2 => Tint.Blue, // Thelodica - 3 => Tint.R, + 3 => Tint.Red, // Default for 0.00rF . _ => average, }; diff --git a/src/Perpetuum/Services/MissionEngine/MissionSpotObjects.cs b/src/Perpetuum/Services/MissionEngine/MissionSpotObjects.cs index 735dcc4e..2757edf2 100644 --- a/src/Perpetuum/Services/MissionEngine/MissionSpotObjects.cs +++ b/src/Perpetuum/Services/MissionEngine/MissionSpotObjects.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Data; -using System.Drawing; -using System.Linq; +using System.Data; using Perpetuum.Data; using Perpetuum.ExportedTypes; using Perpetuum.Services.MissionEngine.MissionDataCacheObjects; @@ -121,7 +118,7 @@ public MissionSpotStat CountSelectableSpots(List allSpotsOnZone) private int CountSelectableByType(MissionSpotType missionSpotType, List spots) { return spots.Count(s => s.type == missionSpotType && - position.ToPoint().ToPosition().TotalDistance2D((Point) s.position.ToPoint()) > 0.5 && + position.ToPoint().ToPosition().TotalDistance2D(s.position.ToPoint()) > 0.5 && position.IsInRangeOf2D(s.position, s.findRadius) && _missionDataCache.IsTargetSelectionValid(Zone, position, s.position)); } diff --git a/src/Perpetuum/Services/MissionEngine/MissionTargets/ZoneMissionTargetObjects.cs b/src/Perpetuum/Services/MissionEngine/MissionTargets/ZoneMissionTargetObjects.cs index 87e49215..065c1338 100644 --- a/src/Perpetuum/Services/MissionEngine/MissionTargets/ZoneMissionTargetObjects.cs +++ b/src/Perpetuum/Services/MissionEngine/MissionTargets/ZoneMissionTargetObjects.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Threading.Tasks; -using Perpetuum.EntityFramework; +using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Items; using Perpetuum.Log; @@ -16,17 +11,18 @@ using Perpetuum.Zones.NpcSystem.Flocks; using Perpetuum.Zones.NpcSystem.Presences; using Perpetuum.Zones.Scanning; +using SkiaSharp; namespace Perpetuum.Services.MissionEngine.MissionTargets { public class LootMissionEventInfo : MissionEventInfo { public Item LootedItem { get; private set; } - public Point LootedPosition { get; private set; } + public SKPointI LootedPosition { get; private set; } public Guid MissionGuid { get; private set; } public int DisplayOrder { get; private set; } - public LootMissionEventInfo(Player player, Item lootedItem, Point lootedPosition, Guid missionGuid, int displayOrder) : base(player) + public LootMissionEventInfo(Player player, Item lootedItem, SKPointI lootedPosition, Guid missionGuid, int displayOrder) : base(player) { LootedItem = lootedItem; LootedPosition = lootedPosition; @@ -108,9 +104,9 @@ protected override Dictionary ToDictionary() public class ReachPositionEventInfo : MissionEventInfo { - public Point ReachedPoint { get; private set; } + public SKPointI ReachedPoint { get; private set; } - public ReachPositionEventInfo(Player player, Point reachedPoint) : base(player) + public ReachPositionEventInfo(Player player, SKPointI reachedPoint) : base(player) { ReachedPoint = reachedPoint; } @@ -154,9 +150,9 @@ protected override void OnHandleMissionEvent(ReachPositionEventInfo e) public class PopNpcEventInfo : MissionEventInfo { - public Point PoppedAtPoint { get; private set; } + public SKPointI PoppedAtPoint { get; private set; } - public PopNpcEventInfo(Player player, Point poppedAtpoint) : base(player) + public PopNpcEventInfo(Player player, SKPointI poppedAtpoint) : base(player) { PoppedAtPoint = poppedAtpoint; } @@ -248,9 +244,9 @@ protected override void OnTargetComplete() public class LockUnitEventInfo : MissionEventInfo { public Npc LockedNpc { get; private set; } - public Point LockedPosition { get; private set; } + public SKPointI LockedPosition { get; private set; } - public LockUnitEventInfo(Player player, Npc lockedUnit, Point lockedPosition) : base(player) + public LockUnitEventInfo(Player player, Npc lockedUnit, SKPointI lockedPosition) : base(player) { LockedNpc = lockedUnit; LockedPosition = lockedPosition; @@ -333,10 +329,10 @@ protected override Dictionary ToDictionary() public class KillEventInfo : MissionEventInfo { - public Point KillPoint { get; private set; } + public SKPointI KillPoint { get; private set; } public Npc KilledNpc { get; private set; } - public KillEventInfo(Player player, Npc killedNpc, Point killPoint) : base(player) + public KillEventInfo(Player player, Npc killedNpc, SKPointI killPoint) : base(player) { KillPoint = killPoint; KilledNpc = killedNpc; @@ -426,9 +422,9 @@ public class ScanMaterialEventInfo : MissionEventInfo { public int ScannedDefinition { get; private set; } public MaterialProbeType ScanProbeType { get; private set; } - public Point ScanPoint { get; private set; } + public SKPointI ScanPoint { get; private set; } - public ScanMaterialEventInfo(Player player, int scannedDefinition, MaterialProbeType probeType, Point scanPoint) : base(player) + public ScanMaterialEventInfo(Player player, int scannedDefinition, MaterialProbeType probeType, SKPointI scanPoint) : base(player) { ScannedDefinition = scannedDefinition; ScanProbeType = probeType; @@ -490,9 +486,9 @@ protected override void OnTargetComplete() public class ScanUnitEventInfo : MissionEventInfo { public Npc ScannedNpc { get; private set; } - public Point ScannedPoint { get; private set; } + public SKPointI ScannedPoint { get; private set; } - public ScanUnitEventInfo(Player player, Npc scannedNpc, Point scannedPoint) : base(player) + public ScanUnitEventInfo(Player player, Npc scannedNpc, SKPointI scannedPoint) : base(player) { ScannedNpc = scannedNpc; ScannedPoint = scannedPoint; @@ -566,9 +562,9 @@ protected override Dictionary ToDictionary() public class ScanContainerEventInfo : MissionEventInfo { public Npc ScannedNpc { get; private set; } - public Point ScanPoint { get; private set; } + public SKPointI ScanPoint { get; private set; } - public ScanContainerEventInfo(Player player, Npc scannedNpc, Point scanPoint) : base(player) + public ScanContainerEventInfo(Player player, Npc scannedNpc, SKPointI scanPoint) : base(player) { ScannedNpc = scannedNpc; ScanPoint = scanPoint; @@ -643,9 +639,9 @@ public class HarvestPlantEventInfo : MissionEventInfo { public int HarvestedDefinition { get;private set; } public int HarvestedQuantity { get; private set; } - public Point HarvestedPoint { get; private set; } + public SKPointI HarvestedPoint { get; private set; } - public HarvestPlantEventInfo(Player player, int harvestedDefinition, int harvestedQuantity, Point harvestedPoint):base(player) + public HarvestPlantEventInfo(Player player, int harvestedDefinition, int harvestedQuantity, SKPointI harvestedPoint):base(player) { HarvestedDefinition = harvestedDefinition; HarvestedQuantity = harvestedQuantity; @@ -727,9 +723,9 @@ public class DrillMineralEventInfo : MissionEventInfo { public int DrilledDefinition { get; private set; } public int DrilledQuantity { get; private set; } - public Point DrillPoint { get; private set; } + public SKPointI DrillPoint { get; private set; } - public DrillMineralEventInfo(Player player, int drilledDefinition, int drilledQuantity, Point drillPoint) : base(player) + public DrillMineralEventInfo(Player player, int drilledDefinition, int drilledQuantity, SKPointI drillPoint) : base(player) { DrilledDefinition = drilledDefinition; DrilledQuantity = drilledQuantity; @@ -814,9 +810,9 @@ public class SubmitItemEventInfo : MissionEventInfo { public Item SubmittedItem { get; private set; } public MissionStructure SubmitMissionStructure { get; private set; } - public Point SubmitPoint { get; private set; } + public SKPointI SubmitPoint { get; private set; } - public SubmitItemEventInfo(Player player, Item submittedItem, MissionStructure submitMissionStructure, Point submitPoint) : base(player) + public SubmitItemEventInfo(Player player, Item submittedItem, MissionStructure submitMissionStructure, SKPointI submitPoint) : base(player) { SubmittedItem = submittedItem; SubmitMissionStructure = submitMissionStructure; @@ -910,9 +906,9 @@ protected override Dictionary ToDictionary() public class SwitchEventInfo : MissionEventInfo { public MissionStructure SwitchMissionStructure { get; private set; } - public Point SwitchPosition { get; private set; } + public SKPointI SwitchPosition { get; private set; } - public SwitchEventInfo(Player player, MissionStructure switchMissionStructure, Point switchPosition) : base(player) + public SwitchEventInfo(Player player, MissionStructure switchMissionStructure, SKPointI switchPosition) : base(player) { SwitchMissionStructure = switchMissionStructure; SwitchPosition = switchPosition; @@ -955,9 +951,9 @@ public class ItemSupplyEventInfo : MissionEventInfo { public Item SuppliedItem { get; private set; } public MissionStructure ItemSupplyStructure { get; private set; } - public Point SupplyPoint { get; private set; } + public SKPointI SupplyPoint { get; private set; } - public ItemSupplyEventInfo(Player player, Item suppliedItem, MissionStructure itemSupplyStructure, Point supplyPoint) : base(player) + public ItemSupplyEventInfo(Player player, Item suppliedItem, MissionStructure itemSupplyStructure, SKPointI supplyPoint) : base(player) { SuppliedItem = suppliedItem; ItemSupplyStructure = itemSupplyStructure; @@ -1032,9 +1028,9 @@ public int GetCurrentProgress() public class FindArtifactEventInfo : MissionEventInfo { public ArtifactType FoundArtifactType { get; private set; } - public Point ArtifactPoint { get; private set; } + public SKPointI ArtifactPoint { get; private set; } - public FindArtifactEventInfo(Player player, ArtifactType foundArtifactType, Point artifactPoint) : base(player) + public FindArtifactEventInfo(Player player, ArtifactType foundArtifactType, SKPointI artifactPoint) : base(player) { FoundArtifactType = foundArtifactType; ArtifactPoint = artifactPoint; @@ -1124,9 +1120,9 @@ protected override void OnTargetComplete() public class SummonEggEventInfo : MissionEventInfo { public int SummonedEggDefinition { get; private set; } - public Point SummonedPoint { get; private set; } + public SKPointI SummonedPoint { get; private set; } - public SummonEggEventInfo(Player player, int summonedEggDefinition, Point summonedPoint) : base(player) + public SummonEggEventInfo(Player player, int summonedEggDefinition, SKPointI summonedPoint) : base(player) { SummonedEggDefinition = summonedEggDefinition; SummonedPoint = summonedPoint; diff --git a/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs b/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs index 594175a0..dd45d16b 100644 --- a/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs +++ b/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; +using SkiaSharp; using System.Threading; namespace Perpetuum.PathFinders @@ -21,7 +21,7 @@ public class AStarFinderReusable /// /// Finds a path using the A* algorithm based on the provided pathfinding information. /// - public Point[]? FindPathHome(IPathFindInfo pathFindInfo, CancellationToken cancellationToken) + public SKPointI[]? FindPathHome(IPathFindInfo pathFindInfo, CancellationToken cancellationToken) { var start = pathFindInfo.Start; var end = pathFindInfo.End; @@ -41,7 +41,7 @@ public class AStarFinderReusable if (start == end) { - return Array.Empty(); + return Array.Empty(); } _openList.Clear(); @@ -84,7 +84,7 @@ public class AStarFinderReusable } [Conditional("DEBUG")] - private static void TestPath(Point[] path, Point start, Point end) + private static void TestPath(SKPointI[] path, SKPointI start, SKPointI end) { if (path.Length < 2) { @@ -105,16 +105,16 @@ private static void TestPath(Point[] path, Point start, Point end) } } - private static Point[] Backtrace(Node node, uint[] list) + private static SKPointI[] Backtrace(Node node, uint[] list) { - var stack = new Stack(); + var stack = new Stack(); int hash = (int)(node.HashCode & 0x3FFFFF); while (true) { int x = hash & 0x7FF; int y = (hash >> 11) & 0x7FF; - stack.Push(new Point(x, y)); + stack.Push(new SKPointI(x, y)); uint parent = list[hash] & 0x3FFFFF; if (parent == (uint)hash) diff --git a/src/Perpetuum/Services/PathFind/IPathFindInfo.cs b/src/Perpetuum/Services/PathFind/IPathFindInfo.cs index 101c52da..d4ffe737 100644 --- a/src/Perpetuum/Services/PathFind/IPathFindInfo.cs +++ b/src/Perpetuum/Services/PathFind/IPathFindInfo.cs @@ -1,5 +1,5 @@ using Perpetuum.PathFinders; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Services.PathFind { @@ -11,11 +11,11 @@ public interface IPathFindInfo /// /// Gets the starting point of the path. /// - Point Start { get; } + SKPointI Start { get; } /// /// Gets the ending point of the path. /// - Point End { get; } + SKPointI End { get; } /// /// Gets the heuristic function for pathfinding. /// diff --git a/src/Perpetuum/Services/PathFind/PathFindInfo.cs b/src/Perpetuum/Services/PathFind/PathFindInfo.cs index 32c8acd9..9a2db1ca 100644 --- a/src/Perpetuum/Services/PathFind/PathFindInfo.cs +++ b/src/Perpetuum/Services/PathFind/PathFindInfo.cs @@ -1,18 +1,18 @@ using Perpetuum.PathFinders; using System; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Services.PathFind { public class PathFindInfo : IPathFindInfo { - private readonly Point _start; - private readonly Point _end; + private readonly SKPointI _start; + private readonly SKPointI _end; private readonly Heuristic _heuristic; private readonly Func _passable; private readonly int _zoneId; - public PathFindInfo(Point start, Point end, Heuristic heuristic, Func passable, int zoneId) + public PathFindInfo(SKPointI start, SKPointI end, Heuristic heuristic, Func passable, int zoneId) { _start = start; _end = end; @@ -21,9 +21,9 @@ public PathFindInfo(Point start, Point end, Heuristic heuristic, Func _start; + public SKPointI Start => _start; - public Point End => _end; + public SKPointI End => _end; public Heuristic Heuristic => _heuristic; @@ -34,14 +34,14 @@ public PathFindInfo(Point start, Point end, Heuristic heuristic, Func _onPathFound; + private readonly Action _onPathFound; - public HomePathFindInfo(Point start, Point end, Heuristic heuristic, Func passable, int zoneId, Action onPathFound) + public HomePathFindInfo(SKPointI start, SKPointI end, Heuristic heuristic, Func passable, int zoneId, Action onPathFound) : base(start, end, heuristic, passable, zoneId) { _onPathFound = onPathFound ?? (_ => { }); } - public Action OnPathFound => _onPathFound; + public Action OnPathFound => _onPathFound; } } diff --git a/src/Perpetuum/Services/Relay/ServerStateInfo.cs b/src/Perpetuum/Services/Relay/ServerStateInfo.cs index 49e5a181..373f0958 100644 --- a/src/Perpetuum/Services/Relay/ServerStateInfo.cs +++ b/src/Perpetuum/Services/Relay/ServerStateInfo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Perpetuum.Data; using Perpetuum.Log; @@ -93,9 +94,21 @@ public void SaveServerInfoToDb(ServerInfo serverInfo) public void PostCurrentServerInfoToWebService() { var serverInfo = GetServerInfo(); - var data = serverInfo.Serialize(); - var reply = Http.Post("http://www.perpetuum-online.com/Server_list", data); - Logger.DebugInfo(reply); + if (serverInfo == null || !serverInfo.IsBroadcast) + { + return; + } + + try + { + var data = serverInfo.Serialize(); + var reply = Http.Post("http://www.perpetuum-online.com/Server_list", data); + Logger.DebugInfo(reply); + } + catch (Exception ex) + { + Logger.Warning($"Failed to post server info to web service: {ex.Message}"); + } } } } diff --git a/src/Perpetuum/Services/Relics/RelicManagers/AbstractRelicManager.cs b/src/Perpetuum/Services/Relics/RelicManagers/AbstractRelicManager.cs index 922a83d0..3d268127 100644 --- a/src/Perpetuum/Services/Relics/RelicManagers/AbstractRelicManager.cs +++ b/src/Perpetuum/Services/Relics/RelicManagers/AbstractRelicManager.cs @@ -1,11 +1,7 @@ using Perpetuum.Zones; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Drawing; using Perpetuum.Log; -using System.Threading; using Perpetuum.Threading; +using SkiaSharp; namespace Perpetuum.Services.Relics { @@ -112,7 +108,7 @@ protected virtual List> DoGetRelicListDictionary() //Abstract methods for extension of behaviour' protected abstract void RefreshBeam(IRelic relic); - protected abstract Point FindRelicPosition(RelicInfo info); + protected abstract SKPointI FindRelicPosition(RelicInfo info); protected abstract RelicInfo GetNextRelicType(); @@ -152,7 +148,7 @@ private void SpawnRelic() } attempts = 0; - Point pt = FindRelicPosition(info); + SKPointI pt = FindRelicPosition(info); while (IsSpawnTooClose(pt) || !IsValidPos(pt)) { pt = FindRelicPosition(info); @@ -166,7 +162,7 @@ private void SpawnRelic() AddRelicToZone(info, pt.ToPosition()); } - private bool IsValidPos(Point pt) + private bool IsValidPos(SKPointI pt) { return Zone.IsWalkable(pt); } @@ -183,7 +179,7 @@ private void AddRelicToZone(RelicInfo info, Position position) } } - private bool IsSpawnTooClose(Point point) + private bool IsSpawnTooClose(SKPointI point) { using (Lock.Read(THREAD_TIMEOUT)) { diff --git a/src/Perpetuum/Services/Relics/RelicManagers/OutpostRelicManager.cs b/src/Perpetuum/Services/Relics/RelicManagers/OutpostRelicManager.cs index 2fd17e9f..46b6ccca 100644 --- a/src/Perpetuum/Services/Relics/RelicManagers/OutpostRelicManager.cs +++ b/src/Perpetuum/Services/Relics/RelicManagers/OutpostRelicManager.cs @@ -1,12 +1,9 @@ using Perpetuum.Zones; -using System; -using System.Collections.Generic; -using System.Drawing; using Perpetuum.ExportedTypes; using Perpetuum.Zones.Beams; using Perpetuum.Zones.Intrusion; using Perpetuum.Zones.Finders.PositionFinders; -using System.Threading; +using SkiaSharp; namespace Perpetuum.Services.Relics { @@ -75,7 +72,7 @@ protected override RelicInfo GetNextRelicType() return _sapRelicInfo; } - protected override Point FindRelicPosition(RelicInfo info) + protected override SKPointI FindRelicPosition(RelicInfo info) { for(int i = 0; i < 10; i++) { @@ -90,7 +87,7 @@ protected override Point FindRelicPosition(RelicInfo info) return p; } } - return Point.Empty; + return SKPointI.Empty; } protected override void RefreshBeam(IRelic relic) diff --git a/src/Perpetuum/Services/Relics/RelicManagers/ZoneRelicManager.cs b/src/Perpetuum/Services/Relics/RelicManagers/ZoneRelicManager.cs index 71112f20..dc7cbf4f 100644 --- a/src/Perpetuum/Services/Relics/RelicManagers/ZoneRelicManager.cs +++ b/src/Perpetuum/Services/Relics/RelicManagers/ZoneRelicManager.cs @@ -3,7 +3,7 @@ using Perpetuum.Zones; using Perpetuum.Zones.Beams; using Perpetuum.Zones.Intrusion; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Services.Relics.RelicManagers { @@ -127,7 +127,7 @@ protected override RelicInfo GetNextRelicType() return info; } - protected override Point FindRelicPosition(RelicInfo info) + protected override SKPointI FindRelicPosition(RelicInfo info) { if (info.HasStaticPosistion) //If the relic spawn info has a valid static position defined - use that { diff --git a/src/Perpetuum/Services/RiftSystem/RiftManager.cs b/src/Perpetuum/Services/RiftSystem/RiftManager.cs index fa2411e8..e717796b 100644 --- a/src/Perpetuum/Services/RiftSystem/RiftManager.cs +++ b/src/Perpetuum/Services/RiftSystem/RiftManager.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Threading; using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Log; @@ -9,6 +5,7 @@ using Perpetuum.Units; using Perpetuum.Zones; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.Services.RiftSystem { @@ -22,12 +19,12 @@ protected RiftSpawnPositionFinder(IZone zone) _zone = zone; } - public Point FindSpawnPosition() + public SKPointI FindSpawnPosition() { return FindSpawnPosition(_zone); } - protected abstract Point FindSpawnPosition(IZone zone); + protected abstract SKPointI FindSpawnPosition(IZone zone); } public class PveRiftSpawnPositionFinder : RiftSpawnPositionFinder @@ -36,7 +33,7 @@ public PveRiftSpawnPositionFinder(IZone zone) : base(zone) { } - protected override Point FindSpawnPosition(IZone zone) + protected override SKPointI FindSpawnPosition(IZone zone) { return zone.GetRandomPassablePosition(); } @@ -48,7 +45,7 @@ public PvpRiftSpawnPositionFinder(IZone zone) : base(zone) { } - protected override Point FindSpawnPosition(IZone zone) + protected override SKPointI FindSpawnPosition(IZone zone) { var p = zone.FindWalkableArea(zone.Size.ToArea(), 20); return p.RandomElement(); diff --git a/src/Perpetuum/SizeExtensions.cs b/src/Perpetuum/SizeExtensions.cs index e80160a1..73f2bfa7 100644 --- a/src/Perpetuum/SizeExtensions.cs +++ b/src/Perpetuum/SizeExtensions.cs @@ -1,31 +1,31 @@ using Perpetuum.Zones; -using System.Drawing; +using SkiaSharp; namespace Perpetuum { public static class SizeExtensions { - public static bool Contains(this Size size, Point p) + public static bool Contains(this SKSizeI size, SKPointI p) { return Contains(size, p.X, p.Y); } - public static bool Contains(this Size size, int x, int y) + public static bool Contains(this SKSizeI size, int x, int y) { return x >= 0 && x < size.Width && y >= 0 && y < size.Height; } - public static Point GetCenter(this Size size) + public static SKPointI GetCenter(this SKSizeI size) { - return new Point(size.Width / 2, size.Height / 2); + return new SKPointI(size.Width / 2, size.Height / 2); } - public static Area ToArea(this Size size) + public static Area ToArea(this SKSizeI size) { return Area.FromRectangle(0, 0, size.Width, size.Height); } - public static Position GetRandomPosition(this Size size, int margin) + public static Position GetRandomPosition(this SKSizeI size, int margin) { int minX = 0 + margin; int maxX = size.Width - margin; @@ -37,25 +37,25 @@ public static Position GetRandomPosition(this Size size, int margin) } [System.Diagnostics.Contracts.Pure] - public static int Ground(this Size size) + public static int Ground(this SKSizeI size) { return size.Width * size.Height; } [System.Diagnostics.Contracts.Pure] - public static T[] CreateArray(this Size size) + public static T[] CreateArray(this SKSizeI size) { return new T[size.Width * size.Height]; } [System.Diagnostics.Contracts.Pure] - public static T[,] Create2DArray(this Size size) + public static T[,] Create2DArray(this SKSizeI size) { return new T[size.Width, size.Height]; } [System.Diagnostics.Contracts.Pure] - public static double Diagonal(this Size size) + public static double Diagonal(this SKSizeI size) { return Math.Sqrt((size.Width * size.Width) + (size.Height * size.Height)); } diff --git a/src/Perpetuum/StateMachines/IState.cs b/src/Perpetuum/StateMachines/IState.cs index 0b3e9348..ac09170f 100644 --- a/src/Perpetuum/StateMachines/IState.cs +++ b/src/Perpetuum/StateMachines/IState.cs @@ -1,5 +1,3 @@ -using System; - namespace Perpetuum.StateMachines { public interface IState diff --git a/src/Perpetuum/Zones/Artifacts/Artifact.cs b/src/Perpetuum/Zones/Artifacts/Artifact.cs index 64aae74f..d6a8a875 100644 --- a/src/Perpetuum/Zones/Artifacts/Artifact.cs +++ b/src/Perpetuum/Zones/Artifacts/Artifact.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Perpetuum.Accounting.Characters; +using Perpetuum.Accounting.Characters; namespace Perpetuum.Zones.Artifacts { diff --git a/src/Perpetuum/Zones/Artifacts/Generators/PersistentArtifactGenerator.cs b/src/Perpetuum/Zones/Artifacts/Generators/PersistentArtifactGenerator.cs index ad5f1ee0..7e970151 100644 --- a/src/Perpetuum/Zones/Artifacts/Generators/PersistentArtifactGenerator.cs +++ b/src/Perpetuum/Zones/Artifacts/Generators/PersistentArtifactGenerator.cs @@ -1,11 +1,10 @@ -using System.Drawing; -using System.Linq; using Perpetuum.Data; using Perpetuum.ExportedTypes; using Perpetuum.Log; using Perpetuum.Players; using Perpetuum.Zones.Artifacts.Repositories; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.Zones.Artifacts.Generators { @@ -74,7 +73,7 @@ private ArtifactType GetNextArtifactType() return ArtifactType.undefined; } - private static Point FindArtifactPosition(IZone zone) + private static SKPointI FindArtifactPosition(IZone zone) { if (!zone.Configuration.Terraformable) { diff --git a/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanResult.cs b/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanResult.cs index 08619385..63dcfcdf 100644 --- a/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanResult.cs +++ b/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanResult.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.Artifacts.Scanners { @@ -8,7 +8,7 @@ namespace Perpetuum.Zones.Artifacts.Scanners public class ArtifactScanResult { public Artifact scannedArtifact; - public Point estimatedPosition; + public SKPointI estimatedPosition; public double radius; } } \ No newline at end of file diff --git a/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanner.cs b/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanner.cs index b330e8b0..32240508 100644 --- a/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanner.cs +++ b/src/Perpetuum/Zones/Artifacts/Scanners/ArtifactScanner.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Perpetuum.ExportedTypes; using Perpetuum.Players; using Perpetuum.Services.Looting; diff --git a/src/Perpetuum/Zones/Beams/BeamBuilder.cs b/src/Perpetuum/Zones/Beams/BeamBuilder.cs index c331b7b3..6e6df6b6 100644 --- a/src/Perpetuum/Zones/Beams/BeamBuilder.cs +++ b/src/Perpetuum/Zones/Beams/BeamBuilder.cs @@ -1,8 +1,7 @@ -using System; -using System.Drawing; using Perpetuum.Builders; using Perpetuum.ExportedTypes; using Perpetuum.Units; +using SkiaSharp; namespace Perpetuum.Zones.Beams { @@ -106,7 +105,7 @@ public BeamBuilder WithDuration(TimeSpan duration) return this; } - public BeamBuilder WithPosition(Point position) + public BeamBuilder WithPosition(SKPointI position) { return WithSourcePosition(position.ToPosition()).WithTargetPosition(position.ToPosition()); } diff --git a/src/Perpetuum/Zones/IZone.cs b/src/Perpetuum/Zones/IZone.cs index f413ef5f..f3a0cd6e 100644 --- a/src/Perpetuum/Zones/IZone.cs +++ b/src/Perpetuum/Zones/IZone.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Drawing; using Perpetuum.Accounting.Characters; using Perpetuum.Common.Loggers; using Perpetuum.Groups.Corporations; @@ -7,7 +5,6 @@ using Perpetuum.Players; using Perpetuum.Services.PathFind; using Perpetuum.Services.Relics; -using Perpetuum.Services.Strongholds; using Perpetuum.Services.Weather; using Perpetuum.Units; using Perpetuum.Zones.Beams; @@ -21,6 +18,7 @@ using Perpetuum.Zones.Terrains.Materials.Plants; using Perpetuum.Zones.Terrains.Terraforming; using Perpetuum.Zones.ZoneEntityRepositories; +using SkiaSharp; namespace Perpetuum.Zones { @@ -30,7 +28,7 @@ public interface IZone bool IsLayerEditLocked { get; set; } int Id { get; } - Size Size { get; } + SKSizeI Size { get; } IEnumerable Units { get; } IEnumerable Players { get; } @@ -43,6 +41,7 @@ public interface IZone Player GetPlayer(long eid); ITerrain Terrain { get; } + CorporationHandler CorporationHandler { get; } IPlantHandler PlantHandler { get; } IBeamService Beams { get; } diff --git a/src/Perpetuum/Zones/Movements/PathMovement.cs b/src/Perpetuum/Zones/Movements/PathMovement.cs index 7af8ad08..e2d85175 100644 --- a/src/Perpetuum/Zones/Movements/PathMovement.cs +++ b/src/Perpetuum/Zones/Movements/PathMovement.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using System.Numerics; using Perpetuum.Units; +using SkiaSharp; namespace Perpetuum.Zones.Movements { @@ -12,7 +9,7 @@ public class PathMovement : Movement private readonly Queue _path = new Queue(); private WaypointMovement _movement; - public PathMovement(IEnumerable path) + public PathMovement(IEnumerable path) { foreach (var point in path.Skip(1)) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/BaseAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/BaseAI.cs index 3e9c326c..68c9641a 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/BaseAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/BaseAI.cs @@ -4,10 +4,7 @@ using Perpetuum.Zones.NpcSystem.AI.Behaviors; using Perpetuum.Zones.NpcSystem.AI.IndustrialDrones; using Perpetuum.Zones.RemoteControl; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; namespace Perpetuum.Zones.NpcSystem.AI { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/CombatAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/CombatAI.cs index c47a4634..922b98c0 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/CombatAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/CombatAI.cs @@ -11,7 +11,7 @@ using Perpetuum.Zones.NpcSystem.TargettingStrategies; using Perpetuum.Zones.NpcSystem.ThreatManaging; using Perpetuum.Zones.Terrains; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -233,7 +233,7 @@ protected virtual void ReturnToHomePosition() WriteLog("Enter evade mode."); } - protected Task> FindNewAttackPositionAsync(Unit hostile) + protected Task> FindNewAttackPositionAsync(Unit hostile) { source?.Cancel(); source = new CancellationTokenSource(); @@ -294,7 +294,7 @@ protected void UpdateHostile(TimeSpan time, bool moveThreatToPseudoThreat = true return; } - List path = t.Result; + List path = t.Result; if (path == null) { @@ -392,9 +392,9 @@ protected virtual bool TryPriorityStrategy(UnitLock[] validLocks) return false; } - private List FindNewAttackPosition(Unit hostile, CancellationToken cancellationToken) + private List FindNewAttackPosition(Unit hostile, CancellationToken cancellationToken) { - Point end = hostile.CurrentPosition.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); + SKPointI end = hostile.CurrentPosition.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); smartCreature.StopMoving(); // Nulling movement so that the unit does not resume it at zero speed if the path is not found. @@ -406,7 +406,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel priorityQueue.Enqueue(startNode); - HashSet closed = + HashSet closed = [ startNode.position ]; @@ -424,7 +424,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel return BuildPath(current); } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -459,7 +459,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel return null; } - private bool IsValidAttackPosition(Unit hostile, Point position) + private bool IsValidAttackPosition(Unit hostile, SKPointI position) { Position position3 = smartCreature.Zone.FixZ(position.ToPosition()).AddToZ(smartCreature.Height); @@ -473,9 +473,9 @@ private bool IsValidAttackPosition(Unit hostile, Point position) return !r.hit; } - private static List BuildPath(Node current) + private static List BuildPath(Node current) { - Stack stack = new(); + Stack stack = new(); Node node = current; while (node != null) diff --git a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/CombatDroneAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/CombatDroneAI.cs index e0f5315e..145aa146 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/CombatDroneAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/CombatDroneAI.cs @@ -6,7 +6,7 @@ using Perpetuum.Zones.Locking.Locks; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.CombatDrones { @@ -131,7 +131,7 @@ protected virtual void ReturnToHomePosition() WriteLog("Enter evade mode."); } - protected Task> FindNewAttackPositionAsync(Unit hostile) + protected Task> FindNewAttackPositionAsync(Unit hostile) { source?.Cancel(); source = new CancellationTokenSource(); @@ -192,7 +192,7 @@ protected void UpdateHostile(TimeSpan time) return; } - List path = t.Result; + List path = t.Result; if (path == null) { @@ -254,9 +254,9 @@ private bool SetLock(UnitLock unitLock) return isNewLock; } - private List FindNewAttackPosition(Unit hostile, CancellationToken cancellationToken) + private List FindNewAttackPosition(Unit hostile, CancellationToken cancellationToken) { - Point end = hostile.CurrentPosition.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); + SKPointI end = hostile.CurrentPosition.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); smartCreature.StopMoving(); // Nulling movement so that the unit does not resume it at zero speed if the path is not found. @@ -268,7 +268,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel priorityQueue.Enqueue(startNode); - HashSet closed = new HashSet + HashSet closed = new HashSet { startNode.position }; @@ -286,7 +286,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel return BuildPath(current); } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -321,7 +321,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel return null; } - private bool IsValidAttackPosition(Unit hostile, Point position) + private bool IsValidAttackPosition(Unit hostile, SKPointI position) { Position position3 = smartCreature.Zone.FixZ(position.ToPosition()).AddToZ(smartCreature.Height); @@ -335,9 +335,9 @@ private bool IsValidAttackPosition(Unit hostile, Point position) return !r.hit; } - private static List BuildPath(Node current) + private static List BuildPath(Node current) { - Stack stack = new Stack(); + Stack stack = new Stack(); Node node = current; while (node != null) diff --git a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/EscortCombatDroneAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/EscortCombatDroneAI.cs index a3f679b1..fe57b217 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/EscortCombatDroneAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/EscortCombatDroneAI.cs @@ -1,6 +1,7 @@ using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.CombatDrones { @@ -29,7 +30,7 @@ public override void Enter() .FindPathAsync(smartCreature.CurrentPosition, randomHome) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/RetreatCombatDroneAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/RetreatCombatDroneAI.cs index 075049c7..5c10a30b 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/RetreatCombatDroneAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/RetreatCombatDroneAI.cs @@ -1,7 +1,7 @@ using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; -using System; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.CombatDrones { @@ -31,7 +31,7 @@ public override void Enter() .FindPathAsync(smartCreature.CurrentPosition, randomHome) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/CoveringAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/CoveringAI.cs index 6a2ca9fe..9f3aaf31 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/CoveringAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/CoveringAI.cs @@ -7,7 +7,7 @@ using Perpetuum.Zones.NpcSystem.AI.Behaviors; using Perpetuum.Zones.NpcSystem.ThreatManaging; using Perpetuum.Zones.Terrains; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -164,7 +164,7 @@ private void UpdateMovement(TimeSpan time) // Snapshot the hostile + screen-target picture on the main thread so // the worker doesn't iterate live ThreatManager/Group/Visibility sets. List hostiles = smartCreature.GetActiveHostiles().ToList(); - Point? screenTarget = ComputeScreenTarget(centroid.Value); + SKPointI? screenTarget = ComputeScreenTarget(centroid.Value); pathPending = true; @@ -177,7 +177,7 @@ private void UpdateMovement(TimeSpan time) return; } - List path = t.Result; + List path = t.Result; if (path == null) { if (!holdLogged) @@ -217,7 +217,7 @@ private void UpdateMovement(TimeSpan time) } } - private Task> FindNewCoverPositionAsync(List hostiles, Point? screenTarget) + private Task> FindNewCoverPositionAsync(List hostiles, SKPointI? screenTarget) { source?.Cancel(); source = new CancellationTokenSource(); @@ -226,10 +226,10 @@ private Task> FindNewCoverPositionAsync(List hostiles, Poin return Task.Run(() => { - List coverPath = FindCoverPosition(hostiles, ct); + List coverPath = FindCoverPosition(hostiles, ct); if (coverPath != null) { - Point goal = coverPath[coverPath.Count - 1]; + SKPointI goal = coverPath[coverPath.Count - 1]; WriteLog($"CoveringAI: cover found at {goal.X},{goal.Y}"); return coverPath; @@ -245,10 +245,10 @@ private Task> FindNewCoverPositionAsync(List hostiles, Poin } WriteLog($"CoveringAI: no cover, falling back to screen at {screenTarget.Value.X},{screenTarget.Value.Y}"); - List screenPath = FindScreenPath(screenTarget.Value, ct); + List screenPath = FindScreenPath(screenTarget.Value, ct); if (screenPath != null) { - Point goal = screenPath[screenPath.Count - 1]; + SKPointI goal = screenPath[screenPath.Count - 1]; WriteLog($"CoveringAI: screen path found at {goal.X},{goal.Y}"); } @@ -290,7 +290,7 @@ private SmartCreature SelectScreenFriendly() // so the friendly's hitbox sits between us and the bulk of incoming fire. // Returns null when there's no friendly to screen behind, or the friendly // is sitting on top of the centroid (degenerate direction). - private Point? ComputeScreenTarget(Position centroid) + private SKPointI? ComputeScreenTarget(Position centroid) { SmartCreature friendly = SelectScreenFriendly(); if (friendly == null) @@ -310,14 +310,14 @@ private SmartCreature SelectScreenFriendly() double tx = friendlyPos.X + (dx / length * ScreenOffsetTiles); double ty = friendlyPos.Y + (dy / length * ScreenOffsetTiles); - return new Point((int)tx, (int)ty); + return new SKPointI((int)tx, (int)ty); } // Worker-thread A* over walkable tiles around the NPC, looking for the nearest // tile from which every active hostile is LoS-blocked by terrain (not plants — // see D4). Modeled on SupportAI.FindSupportPosition; must not mutate AI fields // directly — results travel through Interlocked.Exchange(ref nextMovement, ...). - private List FindCoverPosition(List hostiles, CancellationToken cancellationToken) + private List FindCoverPosition(List hostiles, CancellationToken cancellationToken) { try { @@ -328,7 +328,7 @@ private List FindCoverPosition(List hostiles, CancellationToken // D5: cap search radius at HomeRange so cover never pulls us off-leash. int coverRadius = (int)Math.Max(1, Math.Min(CoverRadius, smartCreature.HomeRange)); - Point origin = smartCreature.CurrentPosition.ToPoint(); + SKPointI origin = smartCreature.CurrentPosition.ToPoint(); double maxNode = Math.Pow(coverRadius, 2) * Math.PI; PriorityQueue priorityQueue = new((int)Math.Max(1, maxNode)); @@ -336,7 +336,7 @@ private List FindCoverPosition(List hostiles, CancellationToken priorityQueue.Enqueue(startNode); - HashSet closed = + HashSet closed = [ startNode.position ]; @@ -353,7 +353,7 @@ private List FindCoverPosition(List hostiles, CancellationToken return BuildPath(current); } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -401,7 +401,7 @@ private List FindCoverPosition(List hostiles, CancellationToken } } - private bool IsValidCoverPosition(Point position, List hostiles) + private bool IsValidCoverPosition(SKPointI position, List hostiles) { IZone zone = smartCreature.Zone; if (zone == null) @@ -444,12 +444,12 @@ private bool IsValidCoverPosition(Point position, List hostiles) // D6 — we trust that "behind a teammate, relative to the threat centroid" // is good enough screening on average. Heuristic biases the search toward // screenTarget so we stop expanding once we reach it. - private List FindScreenPath(Point screenTarget, CancellationToken cancellationToken) + private List FindScreenPath(SKPointI screenTarget, CancellationToken cancellationToken) { try { int coverRadius = (int)Math.Max(1, Math.Min(CoverRadius, smartCreature.HomeRange)); - Point origin = smartCreature.CurrentPosition.ToPoint(); + SKPointI origin = smartCreature.CurrentPosition.ToPoint(); double maxNode = Math.Pow(coverRadius, 2) * Math.PI; PriorityQueue priorityQueue = new((int)Math.Max(1, maxNode)); @@ -457,7 +457,7 @@ private List FindScreenPath(Point screenTarget, CancellationToken cancell priorityQueue.Enqueue(startNode); - HashSet closed = + HashSet closed = [ startNode.position ]; @@ -474,7 +474,7 @@ private List FindScreenPath(Point screenTarget, CancellationToken cancell return BuildPath(current); } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -519,7 +519,7 @@ private List FindScreenPath(Point screenTarget, CancellationToken cancell } } - private static bool IsAtScreenTarget(Point candidate, Point screenTarget) + private static bool IsAtScreenTarget(SKPointI candidate, SKPointI screenTarget) { int dx = Math.Abs(candidate.X - screenTarget.X); int dy = Math.Abs(candidate.Y - screenTarget.Y); @@ -527,9 +527,9 @@ private static bool IsAtScreenTarget(Point candidate, Point screenTarget) return dx <= ScreenArriveTolerance && dy <= ScreenArriveTolerance; } - private static List BuildPath(Node current) + private static List BuildPath(Node current) { - Stack stack = new(); + Stack stack = new(); Node node = current; while (node != null) diff --git a/src/Perpetuum/Zones/NpcSystem/AI/FleeAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/FleeAI.cs index 79d3e71d..60f0d2f6 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/FleeAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/FleeAI.cs @@ -4,6 +4,7 @@ using Perpetuum.Robots; using Perpetuum.Zones.Movements; using Perpetuum.Zones.NpcSystem.ThreatManaging; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -111,7 +112,7 @@ private void StartRetreatPath() .FindPathAsync(start, destination) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterApproachAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterApproachAI.cs index 76b29c36..d916d369 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterApproachAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterApproachAI.cs @@ -3,6 +3,7 @@ using Perpetuum.Timers; using Perpetuum.Units; using Perpetuum.Zones.Movements; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.HunterDrones { @@ -63,7 +64,7 @@ private void RepathToTarget() .FindPathAsync(smartCreature.CurrentPosition, target.CurrentPosition) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { return; diff --git a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterRetreatAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterRetreatAI.cs index 034f55bc..f6cbcedb 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterRetreatAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterRetreatAI.cs @@ -2,6 +2,7 @@ using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.HunterDrones { @@ -30,7 +31,7 @@ public override void Enter() .FindPathAsync(smartCreature.CurrentPosition, randomHome) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { path = new AStarFinder(Heuristic.Manhattan, (x, y) => true) diff --git a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterSelfDestructAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterSelfDestructAI.cs index 5dbb3938..8a9a68bc 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterSelfDestructAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/HunterDrones/HunterSelfDestructAI.cs @@ -5,6 +5,7 @@ using Perpetuum.Units; using Perpetuum.Zones.Effects; using Perpetuum.Zones.Movements; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.HunterDrones { @@ -93,7 +94,7 @@ private void RepathToTarget() .FindPathAsync(smartCreature.CurrentPosition, target.CurrentPosition) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { return; diff --git a/src/Perpetuum/Zones/NpcSystem/AI/IdleAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/IdleAI.cs index cd3a4246..e8aab942 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/IdleAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/IdleAI.cs @@ -1,4 +1,4 @@ -using Perpetuum.Modules; +using Perpetuum.Modules; using Perpetuum.Zones.Movements; using System; diff --git a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialAI.cs index 80a5e303..5fa8905b 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialAI.cs @@ -6,12 +6,7 @@ using Perpetuum.Zones.Movements; using Perpetuum.Zones.NpcSystem.AI.IndustrialDrones; using Perpetuum.Zones.RemoteControl; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -137,7 +132,7 @@ protected void UpdateIndustrialTarget(TimeSpan time) return; } - List path = t.Result; + List path = t.Result; if (path == null) { @@ -177,7 +172,7 @@ protected void EjectCargo(TimeSpan time) } } - protected Task> FindNewAttackPositionAsync(Position position) + protected Task> FindNewAttackPositionAsync(Position position) { source?.Cancel(); source = new CancellationTokenSource(); @@ -226,9 +221,9 @@ private bool SetLock(TerrainLock terrainLock) return isNewLock; } - private List FindNewAttackPosition(Position position, CancellationToken cancellationToken) + private List FindNewAttackPosition(Position position, CancellationToken cancellationToken) { - Point end = position.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); + SKPointI end = position.GetRandomPositionInRange2D(0, smartCreature.BestActionRange - 1).ToPoint(); smartCreature.StopMoving(); // Nulling movement so that the unit does not resume it at zero speed if the path is not found. @@ -240,7 +235,7 @@ private List FindNewAttackPosition(Position position, CancellationToken c priorityQueue.Enqueue(startNode); - HashSet closed = new HashSet + HashSet closed = new HashSet { startNode.position }; @@ -258,7 +253,7 @@ private List FindNewAttackPosition(Position position, CancellationToken c return BuildPath(current); } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -293,7 +288,7 @@ private List FindNewAttackPosition(Position position, CancellationToken c return null; } - private bool IsValidAttackPosition(Position targetPosition, Point position) + private bool IsValidAttackPosition(Position targetPosition, SKPointI position) { Position position3 = smartCreature.Zone.FixZ(position.ToPosition()).AddToZ(smartCreature.Height); @@ -307,9 +302,9 @@ private bool IsValidAttackPosition(Position targetPosition, Point position) return !r.hit; } - private static List BuildPath(Node current) + private static List BuildPath(Node current) { - Stack stack = new Stack(); + Stack stack = new Stack(); Node node = current; while (node != null) diff --git a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/EscortIndustrialDroneAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/EscortIndustrialDroneAI.cs index 15161373..bcc2aef0 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/EscortIndustrialDroneAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/EscortIndustrialDroneAI.cs @@ -1,7 +1,7 @@ using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; -using System; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.IndustrialDrones { @@ -30,7 +30,7 @@ public override void Enter() .FindPathAsync(smartCreature.CurrentPosition, randomHome) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/RetreatIndustrialDroneAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/RetreatIndustrialDroneAI.cs index d5118144..a8a5b50d 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/RetreatIndustrialDroneAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/IndustrialDrones/RetreatIndustrialDroneAI.cs @@ -1,7 +1,7 @@ using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; using Perpetuum.Zones.RemoteControl; -using System; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI.IndustrialDrones { @@ -31,7 +31,7 @@ public override void Enter() .FindPathAsync(smartCreature.CurrentPosition, randomHome) .ContinueWith(t => { - System.Drawing.Point[] path = t.Result; + SKPointI[] path = t.Result; if (path == null) { diff --git a/src/Perpetuum/Zones/NpcSystem/AI/Node.cs b/src/Perpetuum/Zones/NpcSystem/AI/Node.cs index 36cb79c4..9a7362c2 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/Node.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/Node.cs @@ -1,16 +1,15 @@ -using System; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { public class Node : IComparable { - public readonly Point position; + public readonly SKPointI position; public Node parent; public int g; public int f; - public Node(Point position) + public Node(SKPointI position) { this.position = position; } diff --git a/src/Perpetuum/Zones/NpcSystem/AI/SupportAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/SupportAI.cs index bcf63240..dc2544fb 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/SupportAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/SupportAI.cs @@ -6,7 +6,7 @@ using Perpetuum.Units; using Perpetuum.Zones.Locking.Locks; using Perpetuum.Zones.Movements; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -300,7 +300,7 @@ private void UpdateMovement(SmartCreature target, TimeSpan time) return; } - List path = t.Result; + List path = t.Result; if (path == null) { return; @@ -353,7 +353,7 @@ private bool HasLineOfSight(Unit target) return r != null && !r.hit; } - private Task> FindNewSupportPositionAsync(Unit target) + private Task> FindNewSupportPositionAsync(Unit target) { // T7: snapshot the screen-positioning inputs on the main thread so the // worker doesn't iterate live ThreatManager / Group / Visibility state. @@ -388,12 +388,12 @@ private List SnapshotFriendlyPositions() // valid tiles, then score `distance(target) + screenBonus` and return the // best — so support bots prefer to sit behind a friendly relative to the // hostile centroid when otherwise equivalent. - private List FindSupportPosition(Unit target, Position? threatCentroid, List friendlyPositions, CancellationToken cancellationToken) + private List FindSupportPosition(Unit target, Position? threatCentroid, List friendlyPositions, CancellationToken cancellationToken) { try { int approachRange = (int)Math.Max(1, supportRange * 0.7); - Point end = target.CurrentPosition.GetRandomPositionInRange2D(0, approachRange).ToPoint(); + SKPointI end = target.CurrentPosition.GetRandomPositionInRange2D(0, approachRange).ToPoint(); double maxNode = Math.Pow(smartCreature.HomeRange, 2) * Math.PI; PriorityQueue priorityQueue = new((int)maxNode); @@ -401,7 +401,7 @@ private List FindSupportPosition(Unit target, Position? threatCentroid, L priorityQueue.Enqueue(startNode); - HashSet closed = + HashSet closed = [ startNode.position ]; @@ -424,7 +424,7 @@ private List FindSupportPosition(Unit target, Position? threatCentroid, L } } - foreach (Point n in current.position.GetNeighbours()) + foreach (SKPointI n in current.position.GetNeighbours()) { if (closed.Contains(n)) { @@ -483,7 +483,7 @@ private List FindSupportPosition(Unit target, Position? threatCentroid, L } } - private static double ScoreCandidate(Point candidate, Position targetPosition, Position? threatCentroid, List friendlyPositions) + private static double ScoreCandidate(SKPointI candidate, Position targetPosition, Position? threatCentroid, List friendlyPositions) { double distance = targetPosition.TotalDistance2D(candidate); double bonus = HasScreeningFriendly(candidate, threatCentroid, friendlyPositions) ? ScreenBonus : 0.0; @@ -491,7 +491,7 @@ private static double ScoreCandidate(Point candidate, Position targetPosition, P return distance + bonus; } - private static bool HasScreeningFriendly(Point candidate, Position? threatCentroid, List friendlyPositions) + private static bool HasScreeningFriendly(SKPointI candidate, Position? threatCentroid, List friendlyPositions) { if (!threatCentroid.HasValue || friendlyPositions == null || friendlyPositions.Count == 0) { @@ -539,7 +539,7 @@ private static bool HasScreeningFriendly(Point candidate, Position? threatCentro return false; } - private bool IsValidSupportPosition(Unit target, Point position) + private bool IsValidSupportPosition(Unit target, SKPointI position) { IZone zone = smartCreature.Zone; if (zone == null) @@ -559,9 +559,9 @@ private bool IsValidSupportPosition(Unit target, Point position) return r != null && !r.hit; } - private static List BuildPath(Node current) + private static List BuildPath(Node current) { - Stack stack = new(); + Stack stack = new(); Node node = current; while (node != null) diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/DynamicPresence.cs b/src/Perpetuum/Zones/NpcSystem/Presences/DynamicPresence.cs index 0842cf6a..1b9809f0 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/DynamicPresence.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/DynamicPresence.cs @@ -1,7 +1,6 @@ -using System; -using System.Linq; using Perpetuum.Timers; using Perpetuum.Zones.NpcSystem.Flocks; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences { diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/ExpiringStaticPresence/RandomSpawningExpiringPresence.cs b/src/Perpetuum/Zones/NpcSystem/Presences/ExpiringStaticPresence/RandomSpawningExpiringPresence.cs index d8dbd10f..f08a7c80 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/ExpiringStaticPresence/RandomSpawningExpiringPresence.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/ExpiringStaticPresence/RandomSpawningExpiringPresence.cs @@ -1,8 +1,7 @@ using Perpetuum.StateMachines; using Perpetuum.Zones.NpcSystem.Presences.PathFinders; -using System; -using System.Drawing; using Perpetuum.Zones.NpcSystem.Presences.ExpiringStaticPresence; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences.RandomExpiringPresence { @@ -15,7 +14,7 @@ public class RandomSpawningExpiringPresence : ExpiringPresence, IRandomStaticPre public Position SpawnOrigin { get; set; } public IRoamingPathFinder PathFinder { get; set; } public override Area Area => Configuration.Area; - public Point CurrentRoamingPosition { get; set; } + public SKPointI CurrentRoamingPosition { get; set; } public RandomSpawningExpiringPresence(IZone zone, IPresenceConfiguration configuration) : base(zone, configuration) { diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/InterzonePresences/InterzonePresence.cs b/src/Perpetuum/Zones/NpcSystem/Presences/InterzonePresences/InterzonePresence.cs index 76b13e6e..62829977 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/InterzonePresences/InterzonePresence.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/InterzonePresences/InterzonePresence.cs @@ -1,9 +1,8 @@ -using System; -using System.Text; -using System.Drawing; +using System.Text; using Perpetuum.StateMachines; using Perpetuum.Zones.NpcSystem.Flocks; using Perpetuum.Zones.NpcSystem.Presences.PathFinders; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences.InterzonePresences { @@ -11,7 +10,7 @@ public class InterzoneRoamingPresence : InterzonePresence, IRoamingPresence { public StackFSM StackFSM { get; } public Position SpawnOrigin { get; set; } - public Point CurrentRoamingPosition { get; set; } + public SKPointI CurrentRoamingPosition { get; set; } public IRoamingPathFinder PathFinder { get; set; } public InterzoneRoamingPresence(IZone zone, IPresenceConfiguration configuration) : base(zone, configuration) { diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/FreeRoamingPathFinder.cs b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/FreeRoamingPathFinder.cs index c098d1fd..1a94cd85 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/FreeRoamingPathFinder.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/FreeRoamingPathFinder.cs @@ -1,14 +1,11 @@ //#define VERBOSE -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using Perpetuum.Collections; using Perpetuum.ExportedTypes; using Perpetuum.Log; using Perpetuum.PathFinders; using Perpetuum.Zones.NpcSystem.Flocks; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences.PathFinders { @@ -56,7 +53,7 @@ private double TryGetMinSlope(IRoamingPresence presence) return ZoneExtensions.MIN_SLOPE; } - public Point FindSpawnPosition(IRoamingPresence presence) + public SKPointI FindSpawnPosition(IRoamingPresence presence) { var homeRange = TryGetMaxHomeRange(presence); var rangeMax = homeRange * 2; @@ -65,7 +62,7 @@ public Point FindSpawnPosition(IRoamingPresence presence) return walkableArea.RandomElement(); } - public Point FindNextRoamingPosition(IRoamingPresence presence) + public SKPointI FindNextRoamingPosition(IRoamingPresence presence) { var minSlope = TryGetMinSlope(presence); var maxHomeRange = TryGetMaxHomeRange(presence); @@ -75,7 +72,7 @@ public Point FindNextRoamingPosition(IRoamingPresence presence) var startNode = new Node(presence.CurrentRoamingPosition); queue.Enqueue(startNode); - var closed = new HashSet {presence.CurrentRoamingPosition}; + var closed = new HashSet {presence.CurrentRoamingPosition}; if (FastRandom.NextDouble() < 0.3) { @@ -128,10 +125,10 @@ public Point FindNextRoamingPosition(IRoamingPresence presence) private struct Node : IComparable { - public readonly Point location; + public readonly SKPointI location; private readonly int _cost; - public Node(Point location,int cost = 0) + public Node(SKPointI location,int cost = 0) { this.location = location; _cost = cost; diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/IRoamingPathFinder.cs b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/IRoamingPathFinder.cs index 2c296c46..19d0185c 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/IRoamingPathFinder.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/IRoamingPathFinder.cs @@ -1,10 +1,10 @@ -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences.PathFinders { public interface IRoamingPathFinder { - Point FindSpawnPosition(IRoamingPresence presence); - Point FindNextRoamingPosition(IRoamingPresence presence); + SKPointI FindSpawnPosition(IRoamingPresence presence); + SKPointI FindNextRoamingPosition(IRoamingPresence presence); } } \ No newline at end of file diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/NormalRoamingPathFinder.cs b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/NormalRoamingPathFinder.cs index de08236e..7d993aa3 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/NormalRoamingPathFinder.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/NormalRoamingPathFinder.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using Perpetuum.Collections; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences.PathFinders { @@ -17,13 +14,13 @@ public NormalRoamingPathFinder(IZone zone) _zone = zone; } - public Point FindSpawnPosition(IRoamingPresence presence) + public SKPointI FindSpawnPosition(IRoamingPresence presence) { var point = _zone.SafeSpawnPoints.GetAll().RandomElement(); return point.Location; } - public Point FindNextRoamingPosition(IRoamingPresence presence) + public SKPointI FindNextRoamingPosition(IRoamingPresence presence) { var homeRange = presence.Flocks.Max(f => f.HomeRange); var rangeMax = homeRange * 2; @@ -88,7 +85,7 @@ public Point FindNextRoamingPosition(IRoamingPresence presence) return startNode.position; } - private int CalculateCost(Point from, Point position) + private int CalculateCost(SKPointI from, SKPointI position) { var dx = Math.Abs(position.X - from.X); var dy = Math.Abs(position.Y - from.Y); @@ -103,7 +100,7 @@ private int CalculateCost(Point from, Point position) return cost; } - private bool IsRoamingPosition(Point roamingPosition) + private bool IsRoamingPosition(SKPointI roamingPosition) { var isRoaming = _zone.Terrain.Controls[roamingPosition.X,roamingPosition.Y].Roaming; var isWalkable = _zone.IsWalkable(roamingPosition.X, roamingPosition.Y); @@ -112,14 +109,14 @@ private bool IsRoamingPosition(Point roamingPosition) private struct Node : IComparable { - public readonly Point position; + public readonly SKPointI position; public int cost; - public Node(int x, int y) : this(new Point(x, y)) + public Node(int x, int y) : this(new SKPointI(x, y)) { } - public Node(Point position) + public Node(SKPointI position) { this.position = position; cost = 0; diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs index c9378515..7c480c4b 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs @@ -1,7 +1,5 @@ using Perpetuum.Zones.NpcSystem.AI; using Perpetuum.Zones.NpcSystem.Flocks; -using System; -using System.Linq; namespace Perpetuum.Zones.NpcSystem.Presences.PathFinders { diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/RoamingPresence.cs b/src/Perpetuum/Zones/NpcSystem/Presences/RoamingPresence.cs index e3a8a580..961e6fba 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/RoamingPresence.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/RoamingPresence.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Drawing; using Perpetuum.StateMachines; using Perpetuum.Zones.NpcSystem.Flocks; using Perpetuum.Zones.NpcSystem.Presences.PathFinders; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.Presences { @@ -11,7 +9,7 @@ public interface IRoamingPresence { StackFSM StackFSM { get; } Position SpawnOrigin { get; set; } - Point CurrentRoamingPosition { get; set; } + SKPointI CurrentRoamingPosition { get; set; } IRoamingPathFinder PathFinder { get; set; } IPresenceConfiguration Configuration { get; } IZone Zone { get; } @@ -25,7 +23,7 @@ public class RoamingPresence : Presence, IRoamingPresence { public StackFSM StackFSM { get; } public Position SpawnOrigin { get; set; } - public Point CurrentRoamingPosition { get; set; } + public SKPointI CurrentRoamingPosition { get; set; } public IRoamingPathFinder PathFinder { get; set; } public override Area Area => Configuration.Area; diff --git a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/ISafeSpawnPointsRepository.cs b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/ISafeSpawnPointsRepository.cs index a3eb2142..2a233516 100644 --- a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/ISafeSpawnPointsRepository.cs +++ b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/ISafeSpawnPointsRepository.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Perpetuum.Zones.NpcSystem.SafeSpawnPoints { public interface ISafeSpawnPointsRepository diff --git a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/NpcSafeSpawnPointsRepository.cs b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/NpcSafeSpawnPointsRepository.cs index d716af9d..3449cb1e 100644 --- a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/NpcSafeSpawnPointsRepository.cs +++ b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/NpcSafeSpawnPointsRepository.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using Perpetuum.Data; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.SafeSpawnPoints { @@ -45,7 +43,7 @@ public IEnumerable GetAll() { Id = r.GetValue("id"), ZoneId = r.GetValue("zoneId"), - Location = new Point(r.GetValue("x"), r.GetValue("y")) + Location = new SKPointI(r.GetValue("x"), r.GetValue("y")) }; return point; diff --git a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/SafeSpawnPoint.cs b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/SafeSpawnPoint.cs index 1873fdce..2a074824 100644 --- a/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/SafeSpawnPoint.cs +++ b/src/Perpetuum/Zones/NpcSystem/SafeSpawnPoints/SafeSpawnPoint.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.SafeSpawnPoints { @@ -7,7 +6,7 @@ public struct SafeSpawnPoint { public int Id { get; set; } public int ZoneId { private get; set; } - public Point Location { get; set; } + public SKPointI Location { get; set; } public IDictionary ToDictionary() { diff --git a/src/Perpetuum/Zones/NpcSystem/SmartCreature.cs b/src/Perpetuum/Zones/NpcSystem/SmartCreature.cs index c93a5821..9b7e6210 100644 --- a/src/Perpetuum/Zones/NpcSystem/SmartCreature.cs +++ b/src/Perpetuum/Zones/NpcSystem/SmartCreature.cs @@ -38,8 +38,9 @@ public class SmartCreature : Creature public const double FleeCoreThreshold = 0.20; public const double FleeCoreRestoreThreshold = 0.80; // Disabled per IMPROVEMENT-044 (player feedback). Flip to re-enable; FleeAI and the - // thresholds above are left intact for later rework/reuse. - private const bool FleeBehaviorEnabled = false; + // thresholds above are left intact for later rework/reuse. static readonly (not const) + // so the disabled branch stays reachable code instead of a CS0162. + private static readonly bool FleeBehaviorEnabled = false; private readonly TimeKeeper debounceBodyPull = new(TimeSpan.FromSeconds(2.5)); private readonly TimeKeeper debounceLockChange = new(TimeSpan.FromSeconds(2.5)); private readonly IntervalTimer pseudoUpdateFreq = new(TimeSpan.FromMilliseconds(650)); diff --git a/src/Perpetuum/Zones/PBS/EnergyWell/PBSEnergyWell.cs b/src/Perpetuum/Zones/PBS/EnergyWell/PBSEnergyWell.cs index 5bf33ccc..5f0483e5 100644 --- a/src/Perpetuum/Zones/PBS/EnergyWell/PBSEnergyWell.cs +++ b/src/Perpetuum/Zones/PBS/EnergyWell/PBSEnergyWell.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using Perpetuum.Items; +using Perpetuum.Items; using Perpetuum.Log; using Perpetuum.Zones.PBS.Reactors; using Perpetuum.Zones.Terrains.Materials; using Perpetuum.Zones.Terrains.Materials.Minerals; +using SkiaSharp; namespace Perpetuum.Zones.PBS.EnergyWell { @@ -124,7 +122,7 @@ private void SaveToDb() DynamicProperties.Update(k.depleted, IsDepleted ? 1 : 0); } - public List ExtractWithinRange(MineralLayer layer, Point location, int range, uint amount) + public List ExtractWithinRange(MineralLayer layer, SKPointI location, int range, uint amount) { var nodes = layer.GetNodesWithinRange(location, range).OrderBy(n => n.Area.SqrDistance(location)); diff --git a/src/Perpetuum/Zones/Position.cs b/src/Perpetuum/Zones/Position.cs index a16d87e9..9091a9d8 100644 --- a/src/Perpetuum/Zones/Position.cs +++ b/src/Perpetuum/Zones/Position.cs @@ -1,6 +1,6 @@ -using Perpetuum.Collections.Spatial; -using System.Drawing; +using Perpetuum.Collections.Spatial; using System.Numerics; +using SkiaSharp; namespace Perpetuum.Zones { @@ -32,9 +32,9 @@ public Position(double x, double y, double z = 0.0) public Position Center => new(intX + 0.5, intY + 0.5, _z); - public bool IsValid(Size size) + public bool IsValid(SKSizeI size) { - return size.Contains(intX, intY) && intZ >= 0 && intZ < short.MaxValue; + return _x >= 0 && _y >= 0 && _z >= 0 && size.Contains(intX, intY) && intZ < short.MaxValue; } public void Normalize() @@ -87,7 +87,7 @@ public double TotalDistance2D(Position p) } [System.Diagnostics.Contracts.Pure] - public double TotalDistance2D(Point p) + public double TotalDistance2D(SKPointI p) { return TotalDistance2D(p.X, p.Y); } @@ -320,7 +320,7 @@ public Position Rotate90CCW() return new Position(_y, -1 * _x, _z); } - public Position Clamp(Size size) + public Position Clamp(SKSizeI size) { return new Position(_x.Clamp(0, size.Width - 1), _y.Clamp(0, size.Height - 1), _z.Clamp(0, short.MaxValue)); } @@ -372,7 +372,7 @@ public Position Clamp(Size size) return new Position(p._x * num, p._y * num, p._z * num); } - public static implicit operator Point(Position p) + public static implicit operator SKPointI(Position p) { return p.ToPoint(); } @@ -424,14 +424,14 @@ public Vector3 ToVector3() return new Vector3((float)_x, (float)_y, (float)_z); } - public Point ToPoint() + public SKPointI ToPoint() { - return new Point((int)_x, (int)_y); + return new SKPointI((int)_x, (int)_y); } - public PointF ToPointF() + public SKPoint ToPointF() { - return new PointF((float)_x, (float)_y); + return new SKPoint((float)_x, (float)_y); } public ulong GetUlongHashCode() @@ -471,7 +471,7 @@ public IEnumerable NonDiagonalNeighbours { -2, 2}, { -1, 2}, { 0, 2 }, { 1, 2 }, { 2, 2 }, }; - public IEnumerable GetEightNeighbours(Size size) + public IEnumerable GetEightNeighbours(SKSizeI size) { return EightNeighbours.Where(np => np.IsValid(size)); } @@ -490,7 +490,7 @@ public IEnumerable EightNeighbours } } - public IEnumerable GetTwentyFourNeighbours(Size size) + public IEnumerable GetTwentyFourNeighbours(SKSizeI size) { return TwentyFourNeighbours.Where(np => np.IsValid(size)); } @@ -536,14 +536,14 @@ public CellCoord ToCellCoord() return CellCoord.FromXY((int)X, (int)Y); } - public double DirectionTo(Point point) + public double DirectionTo(SKPointI point) { - return DirectionTo(point.ToPosition()); + return DirectionTo(new Position(point.X, point.Y)); } - public bool IsInRangeOf2D(Point point, double range) + public bool IsInRangeOf2D(SKPointI point, double range) { - return IsInRangeOf2D(point.ToPosition(), range); + return IsInRangeOf2D(new Position(point.X, point.Y), range); } diff --git a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Artifact.cs b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Artifact.cs index 55e7e4f7..876a19b1 100644 --- a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Artifact.cs +++ b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Artifact.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; -using System.Transactions; +using System.Transactions; using Perpetuum.Data; using Perpetuum.EntityFramework; using Perpetuum.Zones.Artifacts.Scanners; diff --git a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Directional.cs b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Directional.cs index 558197a4..d09f3d0a 100644 --- a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Directional.cs +++ b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Directional.cs @@ -1,8 +1,8 @@ -using System.Drawing; -using Perpetuum.EntityFramework; +using Perpetuum.EntityFramework; using Perpetuum.Zones.Scanning.Ammos; using Perpetuum.Zones.Terrains; using Perpetuum.Zones.Terrains.Materials; +using SkiaSharp; namespace Perpetuum.Zones.Scanning.Scanners { @@ -17,7 +17,7 @@ public void Visit(DirectionalScannerAmmo ammo) var layer = _zone.Terrain.GetMineralLayerOrThrow(ammo.MaterialType); - var nearestMineralPosition = Point.Empty; + var nearestMineralPosition = SKPointI.Empty; var nearestDist = int.MaxValue; foreach (var node in layer.Nodes) @@ -52,12 +52,12 @@ private double RandomizeDirection(double direction) return direction; } - private static Packet BuildPacket(MaterialType materialType, Position fromPosition, Point nearestMineralPosition, double direction, bool isInRange) + private static Packet BuildPacket(MaterialType materialType, Position fromPosition, SKPointI nearestMineralPosition, double direction, bool isInRange) { var packet = new Packet(ZoneCommand.ScanMineralDirectionalResult); packet.AppendInt((int) materialType); packet.AppendPoint(fromPosition); - packet.AppendBool(nearestMineralPosition != Point.Empty); + packet.AppendBool(nearestMineralPosition != SKPointI.Empty); packet.AppendByte((byte) (direction*255)); packet.AppendBool(isInRange); return packet; diff --git a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Intrusion.cs b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Intrusion.cs index 572e6abb..adc5de30 100644 --- a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Intrusion.cs +++ b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.Intrusion.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using Perpetuum.EntityFramework; +using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Units; using Perpetuum.Zones.Intrusion; diff --git a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.OneTile.cs b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.OneTile.cs index 2261f8c8..ca3cef14 100644 --- a/src/Perpetuum/Zones/Scanning/Scanners/Scanner.OneTile.cs +++ b/src/Perpetuum/Zones/Scanning/Scanners/Scanner.OneTile.cs @@ -1,8 +1,7 @@ -using System.Drawing; -using System.Linq; -using Perpetuum.EntityFramework; +using Perpetuum.EntityFramework; using Perpetuum.Zones.Scanning.Ammos; using Perpetuum.Zones.Terrains.Materials.Minerals; +using SkiaSharp; namespace Perpetuum.Zones.Scanning.Scanners { @@ -17,7 +16,7 @@ public void Visit(OneTileScannerAmmo ammo) OnMineralScanned(MaterialProbeType.OneTile); } - private Packet BuildScanOneTileResultPacket(Point location) + private Packet BuildScanOneTileResultPacket(SKPointI location) { var packet = new Packet(ZoneCommand.ScanOneTileResult); packet.AppendLong(_module.Eid); //module EID diff --git a/src/Perpetuum/Zones/Teleporting/Strategies/TeleportWithinZone.cs b/src/Perpetuum/Zones/Teleporting/Strategies/TeleportWithinZone.cs index 17c51a70..74856af3 100644 --- a/src/Perpetuum/Zones/Teleporting/Strategies/TeleportWithinZone.cs +++ b/src/Perpetuum/Zones/Teleporting/Strategies/TeleportWithinZone.cs @@ -54,15 +54,20 @@ public Task DoTeleportAsync(Player player) player.RemoveFromZone(); player.CurrentSpeed = 0.0; player.AddToZone(zone, validPosition, ZoneEnterType.LocalTeleport); - player.SendInitSelf(); - if (ApplyTeleportSickness) + // Same entry-snapshot/tick interleaving as zone auth: serialize it. + lock (player.UpdateLock) { - player.ApplyTeleportSicknessEffect(); - } - if (ApplyInvulnerable) - { - player.ApplyInvulnerableEffect(); + player.SendInitSelf(); + + if (ApplyTeleportSickness) + { + player.ApplyTeleportSicknessEffect(); + } + if (ApplyInvulnerable) + { + player.ApplyInvulnerableEffect(); + } } } catch (Exception ex) diff --git a/src/Perpetuum/Zones/Terrains/BlockingInfo.cs b/src/Perpetuum/Zones/Terrains/BlockingInfo.cs index 237f4d9a..563c9947 100644 --- a/src/Perpetuum/Zones/Terrains/BlockingInfo.cs +++ b/src/Perpetuum/Zones/Terrains/BlockingInfo.cs @@ -1,5 +1,3 @@ -using System; - namespace Perpetuum.Zones.Terrains { [Serializable] diff --git a/src/Perpetuum/Zones/Terrains/ITerrain.cs b/src/Perpetuum/Zones/Terrains/ITerrain.cs index 60aff867..dd1df3b3 100644 --- a/src/Perpetuum/Zones/Terrains/ITerrain.cs +++ b/src/Perpetuum/Zones/Terrains/ITerrain.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Perpetuum.Zones.Terrains.Materials; using Perpetuum.Zones.Terrains.Materials.Plants; diff --git a/src/Perpetuum/Zones/Terrains/IUpdateableLayer.cs b/src/Perpetuum/Zones/Terrains/IUpdateableLayer.cs index 6984bd79..3ffbb35f 100644 --- a/src/Perpetuum/Zones/Terrains/IUpdateableLayer.cs +++ b/src/Perpetuum/Zones/Terrains/IUpdateableLayer.cs @@ -1,5 +1,3 @@ -using System.IO; - namespace Perpetuum.Zones.Terrains { public interface IUpdateableLayer diff --git a/src/Perpetuum/Zones/Terrains/Layer.cs b/src/Perpetuum/Zones/Terrains/Layer.cs index 3c2f6725..4d5b9332 100644 --- a/src/Perpetuum/Zones/Terrains/Layer.cs +++ b/src/Perpetuum/Zones/Terrains/Layer.cs @@ -143,15 +143,36 @@ public void SetArea(Area area, T[] data) public T GetValue(int x, int y) { - Debug.Assert(x >= 0 && x < Width && y >= 0 && y < Height, "invalid position!"); - return RawData[y * Width + x]; + if (RawData == null || RawData.Length == 0 || Width <= 0 || Height <= 0) + return default; + + x = Math.Clamp(x, 0, Width - 1); + y = Math.Clamp(y, 0, Height - 1); + int idx = y * Width + x; + if (idx < 0 || idx >= RawData.Length) + { + return default; + } + + return RawData[idx]; } public void SetValue(int x, int y, T value) { - Debug.Assert(x >= 0 && x < Width && y >= 0 && y < Height, "invalid position!"); + if (RawData == null || RawData.Length == 0 || Width <= 0 || Height <= 0) + return; + + if (x < 0 || x >= Width || y < 0 || y >= Height) + return; + + int idx = y * Width + x; + if (idx < 0 || idx >= RawData.Length) + { + return; + } + OnUpdating(x, y, ref value); - RawData[y * Width + x] = value; + RawData[idx] = value; OnUpdated(x, y); } diff --git a/src/Perpetuum/Zones/Terrains/LayerExtensions.cs b/src/Perpetuum/Zones/Terrains/LayerExtensions.cs index f5b01f01..1563ccfe 100644 --- a/src/Perpetuum/Zones/Terrains/LayerExtensions.cs +++ b/src/Perpetuum/Zones/Terrains/LayerExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Drawing; +using SkiaSharp; using System.Numerics; namespace Perpetuum.Zones.Terrains @@ -11,7 +10,7 @@ public static bool IsValidPosition(this ILayer layer, int x, int y) return x >= 0 && x < layer.Width && y >= 0 && y < layer.Height; } - public static T GetValue(this ILayer layer, Point position) + public static T GetValue(this ILayer layer, SKPointI position) { return layer.GetValue(position.X, position.Y); } @@ -35,7 +34,7 @@ public static void UpdateAll(this ILayer layer, Func update { for (var y = 0; y < layer.Height; y++) { - for (var x = 0; x < layer.Height; x++) + for (var x = 0; x < layer.Width; x++) { var current = layer[x, y]; var updated = updater(x, y, current); diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/MineralNodeGeneratorBase.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/MineralNodeGeneratorBase.cs index a3d8805f..696ac5cd 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/MineralNodeGeneratorBase.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/MineralNodeGeneratorBase.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using Perpetuum.Units; using Perpetuum.Units.DockingBases; using Perpetuum.Zones.Finders.PositionFinders; using Perpetuum.Zones.Teleporting; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals.Generators { @@ -34,11 +31,11 @@ public MineralNode Generate(MineralLayer layer) return node; } - private Dictionary NormalizeNoise(Dictionary noise) + private Dictionary NormalizeNoise(Dictionary noise) { var max = noise.Values.Max(); - var result = new Dictionary(); + var result = new Dictionary(); foreach (var kvp in noise) { @@ -48,9 +45,9 @@ private Dictionary NormalizeNoise(Dictionary noise return result; } - protected abstract Dictionary GenerateNoise(Position startPosition); + protected abstract Dictionary GenerateNoise(Position startPosition); - protected bool IsValid(Point location) + protected bool IsValid(SKPointI location) { if (!_zone.Size.Contains(location.X, location.Y)) return false; @@ -77,7 +74,7 @@ protected bool IsValid(Point location) return true; } - private MineralNode CreateMineralNode(MineralLayer layer, Dictionary tiles) + private MineralNode CreateMineralNode(MineralLayer layer, Dictionary tiles) { int minx = int.MaxValue, miny = int.MaxValue, maxx = 0, maxy = 0; @@ -108,7 +105,7 @@ private MineralNode CreateMineralNode(MineralLayer layer, Dictionary().WithinRange2D(location.ToPosition(), dist).Any()) return true; diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/RandomWalkMineralNodeGenerator.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/RandomWalkMineralNodeGenerator.cs index 6e51c149..24960f1f 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/RandomWalkMineralNodeGenerator.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/Generators/RandomWalkMineralNodeGenerator.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals.Generators { @@ -13,29 +11,29 @@ public RandomWalkMineralNodeGenerator(IZone zone) : base(zone) private int BrushSize { get; set; } - protected override Dictionary GenerateNoise(Position startPosition) + protected override Dictionary GenerateNoise(Position startPosition) { - var closed = new HashSet(); - var tiles = new Dictionary(); + var closed = new HashSet(); + var tiles = new Dictionary(); - var q = new Queue(); + var q = new Queue(); q.Enqueue(startPosition); var i = 0; - while (q.TryDequeue(out Point current) && i < 50000) + while (q.TryDequeue(out SKPointI current) && i < 50000) { i++; foreach (var point in current.FloodFill(IsValid).Take(BrushSize * BrushSize)) { - tiles.AddOrUpdate(point,1,c => c + 1); + tiles.AddOrUpdate(point, 1, c => c + 1); if (tiles.Count >= MaxTiles) return tiles; } - var r = new List(); + var r = new List(); foreach (var np in current.GetNeighbours()) { diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/GravelLayer.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/GravelLayer.cs index b8083c04..b4eb58ac 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/GravelLayer.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/GravelLayer.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; using Perpetuum.IO; -using Perpetuum.Services.EventServices; using Perpetuum.Zones.Terrains.Materials.Minerals.Generators; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals { @@ -51,9 +47,9 @@ public void Delete(MineralNode node) public List GetAll() { - var bmp = (Bitmap)Image.FromFile(_fileSystem.CreatePath( Path.Combine("layers", "mineral_gravel.0045.png"))); + var bmp = SKBitmap.Decode(_fileSystem.CreatePath(Path.Combine("layers", "mineral_gravel.0045.png"))); - var minerals = new Dictionary(); + var minerals = new Dictionary(); int minx = int.MaxValue, miny = int.MaxValue, maxx = 0, maxy = 0; @@ -63,7 +59,7 @@ public List GetAll() { var c = bmp.GetPixel(x, y); - var b = c.GetBrightness() * 350000; + var b = c.GetLuminance() * 350000; if (b > 0) { @@ -72,7 +68,7 @@ public List GetAll() maxx = Math.Max(maxx, x); maxy = Math.Max(maxy, y); - var point = new Point(x, y); + var point = new SKPointI(x, y); minerals.Add(point, (uint)b); } } diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralExtractor.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralExtractor.cs index 5a09f42a..2a79d1a8 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralExtractor.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralExtractor.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Drawing; using Perpetuum.Collections; using Perpetuum.Items; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals { public class MineralExtractor : MineralLayerVisitor { - private readonly Point _location; + private readonly SKPointI _location; private readonly uint _amount; private readonly MaterialHelper _materialHelper; @@ -16,7 +14,7 @@ public class MineralExtractor : MineralLayerVisitor public List Items => _items; - public MineralExtractor(Point location,uint amount,MaterialHelper materialHelper) + public MineralExtractor(SKPointI location,uint amount,MaterialHelper materialHelper) { _location = location; _amount = amount; @@ -48,10 +46,10 @@ public override void VisitOreLayer(OreLayer layer) private struct MineralDistance : IComparable { - public readonly Point location; + public readonly SKPointI location; private readonly int _sqrDistance; - public MineralDistance(Point location, int sqrDistance) + public MineralDistance(SKPointI location, int sqrDistance) { this.location = location; _sqrDistance = sqrDistance; @@ -79,7 +77,7 @@ public override void VisitLiquidLayer(LiquidLayer layer) continue; var d = _location.SqrDistance(x, y); - pq.Enqueue(new MineralDistance(new Point(x, y), d)); + pq.Enqueue(new MineralDistance(new SKPointI(x, y), d)); } } diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralLayer.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralLayer.cs index 6418ce83..3c29993a 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralLayer.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralLayer.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Drawing; using Perpetuum.Log; using Perpetuum.Services.EventServices; using Perpetuum.Services.EventServices.EventMessages; using Perpetuum.Timers; using Perpetuum.Zones.Terrains.Materials.Minerals.Actions; using Perpetuum.Zones.Terrains.Materials.Minerals.Generators; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals { @@ -81,7 +79,7 @@ public void GenerateNewNode() RunAction(new GenerateMineralNode(generator)); } - public List GetNodesWithinRange(Point location, int range) + public List GetNodesWithinRange(SKPointI location, int range) { var area = Area.FromRadius(location.X, location.Y, range); return GetNodesByArea(area); @@ -100,7 +98,7 @@ public List GetNodesByArea(Area area) return nodes; } - public MineralNode GetNearestNode(Point p) + public MineralNode GetNearestNode(SKPointI p) { MineralNode nearestNode = null; var nearestDistSq = double.MaxValue; @@ -199,7 +197,7 @@ private void OnNodeExpired(MineralNode node) } [CanBeNull] - public MineralNode GetNode(Point p) + public MineralNode GetNode(SKPointI p) { MineralNode node; if (!TryGetNode(p, out node)) @@ -208,7 +206,7 @@ public MineralNode GetNode(Point p) return node; } - public bool TryGetNode(Point p, out MineralNode node) + public bool TryGetNode(SKPointI p, out MineralNode node) { return TryGetNode(p.X, p.Y, out node); } @@ -243,7 +241,7 @@ public void WriteLog(string message) Logger.Info($"Mineral ({_configuration.ZoneId}:{Type}) {message}"); } - public bool HasMineral(Point location) + public bool HasMineral(SKPointI location) { var node = GetNode(location); if (node == null) diff --git a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralNode.cs b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralNode.cs index 9180150d..2291279e 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralNode.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Minerals/MineralNode.cs @@ -1,8 +1,7 @@ -using System; using System.Diagnostics; -using System.Drawing; using Perpetuum.Threading; using Perpetuum.Timers; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Materials.Minerals { @@ -86,7 +85,7 @@ private int GetOffset(int x, int y) return offset; } - public bool HasValue(Point p) + public bool HasValue(SKPointI p) { return HasValue(p.X, p.Y); } @@ -97,7 +96,7 @@ public bool HasValue(int x, int y) return v > 0; } - public uint DecreaseValue(Point p, uint value) + public uint DecreaseValue(SKPointI p, uint value) { OnDecrease(); return DecreaseValue(p.X, p.Y, value); @@ -150,7 +149,7 @@ public ulong GetTotalAmount() return sum; } - public uint GetValue(Point p) + public uint GetValue(SKPointI p) { return GetValue(p.X, p.Y); } @@ -166,7 +165,7 @@ public uint GetValue(int x, int y) return value; } - public void SetValue(Point p,uint value) + public void SetValue(SKPointI p,uint value) { SetValue(p.X,p.Y,value); } @@ -212,9 +211,9 @@ public void Update(TimeSpan time) OnUpdated(); } - public Point GetNearestMineralPosition(Point p) + public SKPointI GetNearestMineralPosition(SKPointI p) { - var nearest = Point.Empty; + var nearest = SKPointI.Empty; var nearestDist = int.MaxValue; var offset = 0; @@ -230,7 +229,7 @@ public Point GetNearestMineralPosition(Point p) if (d >= nearestDist) continue; - nearest = new Point(ax,ay); + nearest = new SKPointI(ax,ay); nearestDist = d; } } diff --git a/src/Perpetuum/Zones/Terrains/Materials/Plants/PlantHandler.cs b/src/Perpetuum/Zones/Terrains/Materials/Plants/PlantHandler.cs index dde5a74d..77f9554b 100644 --- a/src/Perpetuum/Zones/Terrains/Materials/Plants/PlantHandler.cs +++ b/src/Perpetuum/Zones/Terrains/Materials/Plants/PlantHandler.cs @@ -1,10 +1,6 @@ using Perpetuum.Log; using Perpetuum.Threading.Process; using Perpetuum.Timers; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; namespace Perpetuum.Zones.Terrains.Materials.Plants { diff --git a/src/Perpetuum/Zones/Terrains/PassableMapBuilder.cs b/src/Perpetuum/Zones/Terrains/PassableMapBuilder.cs index 726f5b31..bd4d5a52 100644 --- a/src/Perpetuum/Zones/Terrains/PassableMapBuilder.cs +++ b/src/Perpetuum/Zones/Terrains/PassableMapBuilder.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using System.Drawing; -using Perpetuum.Builders; +using Perpetuum.Builders; using Perpetuum.Log; +using SkiaSharp; namespace Perpetuum.Zones.Terrains { @@ -11,7 +10,7 @@ public class PassableMapBuilder : IBuilder> private readonly SlopeLayer _slopeLayer; private readonly IEnumerable _startPositions; - private Size _size; + private SKSizeI _size; public PassableMapBuilder(ILayer blocksLayer,SlopeLayer slopeLayer,IEnumerable startPositions) { @@ -19,7 +18,7 @@ public PassableMapBuilder(ILayer blocksLayer,SlopeLayer slopeLayer _slopeLayer = slopeLayer; _startPositions = startPositions; - _size = new Size(blocksLayer.Width, blocksLayer.Height); + _size = new SKSizeI(blocksLayer.Width, blocksLayer.Height); } public ILayer Build() diff --git a/src/Perpetuum/Zones/Terrains/Terraforming/Operations/BlurTerraformingOperation.cs b/src/Perpetuum/Zones/Terrains/Terraforming/Operations/BlurTerraformingOperation.cs index a271b2d2..93dc720b 100644 --- a/src/Perpetuum/Zones/Terrains/Terraforming/Operations/BlurTerraformingOperation.cs +++ b/src/Perpetuum/Zones/Terrains/Terraforming/Operations/BlurTerraformingOperation.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Terraforming.Operations { @@ -17,7 +17,7 @@ public override void AcceptVisitor(TerraformingOperationVisitor visitor) protected override int ProduceDirection(IZone zone, int x, int y) { - var p = new Point(x, y); + var p = new SKPointI(x, y); var sum = 0.0; foreach (var n in p.GetNeighbours()) diff --git a/src/Perpetuum/Zones/Terrains/Terraforming/TerraformHandler.cs b/src/Perpetuum/Zones/Terrains/Terraforming/TerraformHandler.cs index 3fe7a0a3..17220abc 100644 --- a/src/Perpetuum/Zones/Terrains/Terraforming/TerraformHandler.cs +++ b/src/Perpetuum/Zones/Terrains/Terraforming/TerraformHandler.cs @@ -1,13 +1,10 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using Perpetuum.ExportedTypes; using Perpetuum.Threading.Process; using Perpetuum.Timers; using Perpetuum.Zones.Beams; using Perpetuum.Zones.Terrains.Terraforming.Operations; +using SkiaSharp; namespace Perpetuum.Zones.Terrains.Terraforming { @@ -152,12 +149,12 @@ private void ProcessAffectedPositions() private void SendAffectedPositions() { - var affectedTiles = new Dictionary(); + var affectedTiles = new Dictionary(); AffectedTile tile; while (_affectedTiles.TryTake(out tile)) { - affectedTiles[new Point(tile.x,tile.y)] = tile.Type; + affectedTiles[new SKPointI(tile.x,tile.y)] = tile.Type; } foreach (var pair in affectedTiles) diff --git a/src/Perpetuum/Zones/Terrains/Terrain.cs b/src/Perpetuum/Zones/Terrains/Terrain.cs index 2debf118..8eb97c8a 100644 --- a/src/Perpetuum/Zones/Terrains/Terrain.cs +++ b/src/Perpetuum/Zones/Terrains/Terrain.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Perpetuum.Zones.Terrains.Materials; using Perpetuum.Zones.Terrains.Materials.Plants; diff --git a/src/Perpetuum/Zones/Terrains/TerrainControlInfo.cs b/src/Perpetuum/Zones/Terrains/TerrainControlInfo.cs index f621b360..1f338708 100644 --- a/src/Perpetuum/Zones/Terrains/TerrainControlInfo.cs +++ b/src/Perpetuum/Zones/Terrains/TerrainControlInfo.cs @@ -7,6 +7,11 @@ public struct TerrainControlInfo : IEquatable { private TerrainControlFlags _flags; + public TerrainControlInfo(TerrainControlFlags flags) + { + _flags = flags; + } + public TerrainControlFlags Flags { get { return _flags; } diff --git a/src/Perpetuum/Zones/Terrains/TerrainUpdateMonitor.cs b/src/Perpetuum/Zones/Terrains/TerrainUpdateMonitor.cs index 2de367be..1d8ce202 100644 --- a/src/Perpetuum/Zones/Terrains/TerrainUpdateMonitor.cs +++ b/src/Perpetuum/Zones/Terrains/TerrainUpdateMonitor.cs @@ -1,6 +1,5 @@ -using System; using System.Collections.Immutable; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones.Terrains { @@ -8,9 +7,9 @@ public abstract class TerrainUpdateInfo { public LayerType Type { get; } - public Point Position { get; } + public SKPointI Position { get; } - protected TerrainUpdateInfo(LayerType type, Point position) + protected TerrainUpdateInfo(LayerType type, SKPointI position) { Position = position; Type = type; @@ -68,7 +67,7 @@ public override int GetHashCode() public class TileUpdateInfo : TerrainUpdateInfo { - public TileUpdateInfo(LayerType type, Point position) + public TileUpdateInfo(LayerType type, SKPointI position) : base(type, position) { } @@ -178,7 +177,7 @@ private void OnAreaUpdated(LayerType layerType, Area area) private void OnTileUpdated(LayerType layerType, int x, int y) { - var info = new TileUpdateInfo(layerType,new Point(x,y)); + var info = new TileUpdateInfo(layerType, new SKPointI(x,y)); AddUpdateInfo(info); } diff --git a/src/Perpetuum/Zones/Terrains/TerrainUpdateNotifier.cs b/src/Perpetuum/Zones/Terrains/TerrainUpdateNotifier.cs index 55274c99..8001d039 100644 --- a/src/Perpetuum/Zones/Terrains/TerrainUpdateNotifier.cs +++ b/src/Perpetuum/Zones/Terrains/TerrainUpdateNotifier.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Perpetuum.Collections.Spatial; using Perpetuum.Log; using Perpetuum.Players; diff --git a/src/Perpetuum/Zones/Zone.cs b/src/Perpetuum/Zones/Zone.cs index eb85a930..9cdc7474 100644 --- a/src/Perpetuum/Zones/Zone.cs +++ b/src/Perpetuum/Zones/Zone.cs @@ -32,8 +32,8 @@ using Perpetuum.Zones.ZoneEntityRepositories; using System.Collections.Immutable; using System.Diagnostics; -using System.Drawing; using System.Net.Sockets; +using SkiaSharp; namespace Perpetuum.Zones { @@ -44,7 +44,7 @@ public abstract class Zone : Threading.Process.Process, IZone private ImmutableDictionary _players = ImmutableDictionary.Empty; public int Id => Configuration.Id; - public Size Size => Configuration.Size; + public SKSizeI Size => Configuration.Size; public IDecorHandler DecorHandler { get; set; } diff --git a/src/Perpetuum/Zones/ZoneConfiguration.cs b/src/Perpetuum/Zones/ZoneConfiguration.cs index 7c386dd2..7a61b95c 100644 --- a/src/Perpetuum/Zones/ZoneConfiguration.cs +++ b/src/Perpetuum/Zones/ZoneConfiguration.cs @@ -2,8 +2,7 @@ using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Zones.Terrains.Materials.Plants; -using System.Collections.Generic; -using System.Drawing; +using SkiaSharp; namespace Perpetuum.Zones { @@ -44,8 +43,8 @@ public IEnumerable GetAll() ZoneConfiguration config = new ZoneConfiguration { Id = id, - WorldPosition = new Point(x, y), - Size = new Size(w, h), + WorldPosition = new SKPointI(x, y), + Size = new SKSizeI(w, h), Name = record.GetValue("name"), Fertility = record.GetValue("fertility"), PluginName = record.GetValue("zoneplugin"), @@ -91,8 +90,8 @@ public sealed class ZoneConfiguration public int plantRuleSetId; public int Id { get; set; } - public Size Size { get; set; } - public Point WorldPosition { get; set; } + public SKSizeI Size { get; set; } + public SKPointI WorldPosition { get; set; } public string PluginName { get; set; } public int Fertility { get; set; } public bool Terraformable { get; set; } diff --git a/src/Perpetuum/Zones/ZoneExtensions.Bitmap.cs b/src/Perpetuum/Zones/ZoneExtensions.Bitmap.cs index 665f3990..a69fa76d 100644 --- a/src/Perpetuum/Zones/ZoneExtensions.Bitmap.cs +++ b/src/Perpetuum/Zones/ZoneExtensions.Bitmap.cs @@ -1,7 +1,6 @@ -using System.Drawing; -using System.Drawing.Imaging; -using Perpetuum.IO; +using Perpetuum.IO; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.Zones { @@ -15,10 +14,11 @@ public SaveBitmapHelper(IFileSystem fileSystem) } - public void SaveBitmap(IZone zone,Bitmap bitmap,string name) + public void SaveBitmap(IZone zone, SKBitmap bitmap, string name) { var fn = _fileSystem.CreatePath("bitmaps",zone.CreateTerrainDataFilename(name,"png")); - bitmap.Save(fn,ImageFormat.Png); + using var stream = File.Create(fn); + bitmap.Encode(stream, SKEncodedImageFormat.Png, 100); } } @@ -26,12 +26,17 @@ public void SaveBitmap(IZone zone,Bitmap bitmap,string name) public static partial class ZoneExtensions { - public static Bitmap CreatePassableBitmap(this IZone zone, Color passableTileColor, Color islandTileColor = default(Color)) + public static SKBitmap CreatePassableBitmap(this IZone zone, SKColor passableTileColor, SKColor islandTileColor = default) { - var skipIsland = islandTileColor.Equals(default(Color)); + var skipIsland = islandTileColor.Equals(default); var b = zone.CreateBitmap(); - b.WithGraphics(g => g.FillRectangle(new SolidBrush(Color.FromArgb(255, 0, 0, 0)), 0, 0, zone.Size.Width - 1, zone.Size.Height - 1)); + var canvas = new SKCanvas(b); + + SKPaint paint = new() { Color = SKColors.Black, Style = SKPaintStyle.Fill }; + b.WithCanvas(g => + g.DrawRect(0, 0, zone.Size.Width - 1, zone.Size.Height - 1, paint) + ); return b.ForEach((bmp, x, y) => { @@ -49,10 +54,10 @@ public static partial class ZoneExtensions }); } - public static Bitmap CreateBitmap(this IZone zone) + public static SKBitmap CreateBitmap(this IZone zone) { var size = zone.Size; - return new Bitmap(size.Width,size.Height,PixelFormat.Format32bppArgb); + return new SKBitmap(size.Width, size.Height, SKColorType.Rgba8888, SKAlphaType.Opaque); } } diff --git a/src/Perpetuum/Zones/ZoneExtensions.Terrain.cs b/src/Perpetuum/Zones/ZoneExtensions.Terrain.cs index 62a04995..18bd8470 100644 --- a/src/Perpetuum/Zones/ZoneExtensions.Terrain.cs +++ b/src/Perpetuum/Zones/ZoneExtensions.Terrain.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using Perpetuum.Data; +using Perpetuum.Data; using Perpetuum.Zones.Terrains; +using SkiaSharp; namespace Perpetuum.Zones { @@ -18,7 +16,7 @@ public static IEnumerable GetPassablePositionFromDb(this IZone zone) .Select(r => new Position(r.GetValue(0), r.GetValue(1))).ToList(); } - public static bool IsWalkableForNpc(this IZone zone,Point position, double slope = MIN_SLOPE) + public static bool IsWalkableForNpc(this IZone zone, SKPointI position, double slope = MIN_SLOPE) { return zone.IsWalkableForNpc(position.X, position.Y, slope); } @@ -36,7 +34,7 @@ public static bool IsWalkable(this IZone zone, int x, int y, double slope = MIN_ return zone.IsWalkable(new Position(x, y), slope); } - public static bool IsWalkable(this IZone zone, Point point, double slope = MIN_SLOPE) + public static bool IsWalkable(this IZone zone, SKPointI point, double slope = MIN_SLOPE) { return zone.IsWalkable(point.ToPosition(),slope); } diff --git a/src/Perpetuum/Zones/ZoneExtensions.cs b/src/Perpetuum/Zones/ZoneExtensions.cs index 0b93a7e8..7f37f886 100644 --- a/src/Perpetuum/Zones/ZoneExtensions.cs +++ b/src/Perpetuum/Zones/ZoneExtensions.cs @@ -7,7 +7,7 @@ using Perpetuum.Units; using Perpetuum.Zones.RemoteControl; using Perpetuum.Zones.Terrains; -using System.Drawing; +using SkiaSharp; using System.Runtime.InteropServices; namespace Perpetuum.Zones @@ -38,7 +38,42 @@ public LayerFileIO(IFileSystem fileSystem) public T[] LoadLayerData(IZone zone, string name) where T : struct { string path = zone.CreateTerrainDataFilename(name); - T[] data = _fileSystem.ReadLayer(path); + T[] data; + + if (typeof(T) == typeof(TerrainControlInfo)) + { + byte[] rawBytes = _fileSystem.ReadLayerAsByteArray(path); + int totalCells = zone.Size.Width * zone.Size.Height; + if (rawBytes.Length == totalCells) + { + var converted = new TerrainControlInfo[totalCells]; + for (int i = 0; i < totalCells; i++) + { + converted[i] = new TerrainControlInfo((TerrainControlFlags)rawBytes[i]); + } + data = (T[])(object)converted; + } + else + { + data = rawBytes.ToArray(); + } + } + else + { + data = _fileSystem.ReadLayer(path); + } + + int expectedCells = zone.Size.Width * zone.Size.Height; + if (expectedCells > 0 && (data == null || data.Length != expectedCells)) + { + var safeData = new T[expectedCells]; + if (data != null && data.Length > 0) + { + Array.Copy(data, safeData, Math.Min(data.Length, expectedCells)); + } + data = safeData; + } + Logger.Info("Layer data loaded. (" + name + ") zone:" + zone.Id); return data; } @@ -64,23 +99,23 @@ public void SaveLayerToDisk(IZone zone, ILayer layer) where T : struct public static partial class ZoneExtensions { - public static List FindWalkableArea(this IZone zone, Area area, int size, double slope = 4.0) + public static List FindWalkableArea(this IZone zone, Area area, int size, double slope = 4.0) { area = area.Clamp(zone.Size); while (true) { - Point startPosition; + SKPointI startPosition; while (true) { startPosition = area.GetRandomPosition(); - if (!zone.Terrain.Blocks.GetValue(startPosition).Island && zone.IsWalkable(startPosition, slope)) + if (!zone.Terrain.Blocks.GetValue(startPosition.X, startPosition.Y).Island && zone.IsWalkable(startPosition, slope)) { break; } } - List p = FindWalkableArea(zone, startPosition, area, size, slope); + List p = FindWalkableArea(zone, startPosition, area, size, slope); if (p != null) { return p; @@ -91,14 +126,14 @@ public static List FindWalkableArea(this IZone zone, Area area, int size, } [CanBeNull] - public static List? FindWalkableArea(this IZone zone, Point startPosition, Area area, int size, double slope = 4.0) + public static List? FindWalkableArea(this IZone zone, SKPointI startPosition, Area area, int size, double slope = 4.0) { - Queue q = new(); + Queue q = new(); q.Enqueue(startPosition); - HashSet closed = [startPosition]; + HashSet closed = [startPosition]; - List result = []; - while (q.TryDequeue(out Point position)) + List result = []; + while (q.TryDequeue(out SKPointI position)) { result.Add(position); @@ -108,7 +143,7 @@ public static List FindWalkableArea(this IZone zone, Area area, int size, return result; } - foreach (Point np in position.GetNonDiagonalNeighbours()) + foreach (SKPointI np in position.GetNonDiagonalNeighbours()) { if (closed.Contains(np)) { @@ -138,7 +173,7 @@ public static List FindWalkableArea(this IZone zone, Area area, int size, /// End point of line segment /// Slope capability check for slope-based blocking /// True if tiles checked are walkable - public static bool CheckLinearPath(this IZone zone, Point start, Point end, double slope = 4.0) + public static bool CheckLinearPath(this IZone zone, SKPointI start, SKPointI end, double slope = 4.0) { int x = start.X; int y = start.Y; diff --git a/src/Perpetuum/Zones/ZoneSession.cs b/src/Perpetuum/Zones/ZoneSession.cs index 3f9049f6..229b77d3 100644 --- a/src/Perpetuum/Zones/ZoneSession.cs +++ b/src/Perpetuum/Zones/ZoneSession.cs @@ -300,10 +300,16 @@ private void HandleAuth(Packet packet) _terrainUpdateNotifier = CreateTerrainNotifier(player); - player.SetSession(this); - player.SendInitSelf(); - player.ApplyTeleportSicknessEffect(); - player.ApplyInvulnerableEffect(); + // The player is already in the zone (added on transaction commit), so the tick thread + // may be updating it concurrently. Serialize the entry snapshot with the tick to keep + // it consistent. + lock (player.UpdateLock) + { + player.SetSession(this); + player.SendInitSelf(); + player.ApplyTeleportSicknessEffect(); + player.ApplyInvulnerableEffect(); + } _player = player; } diff --git a/template/perpetuum.ini.template b/template/perpetuum.ini.template new file mode 100644 index 00000000..c4b180e9 --- /dev/null +++ b/template/perpetuum.ini.template @@ -0,0 +1,18 @@ +{ + "ListenerPort": {SERVER_PORT}, + "EnableUpnp": false, + "EnableDev": true, + + "PersonalConfig": "startup_standalone", + "ConnectionString": "{CONNECTION_STRING}", + + "Corporation":{ + "Price" : 250000, + "HangarPrice" : 50000, + "RentPeriod" : 7, + "LeavePeriod" : 1440, + "NumberOfHangarFolders" : 10, + "FoundingPrice" : 25000 + }, + "ResourceServerURL": "{ASSET_URL}" +} \ No newline at end of file diff --git a/template/restore_DB_to_original_state.sql b/template/restore_DB_to_original_state.sql new file mode 100644 index 00000000..8ec0c097 --- /dev/null +++ b/template/restore_DB_to_original_state.sql @@ -0,0 +1,17 @@ +-- Altered restore DB SQL script that can run inside a container +-- Assumes the 'perpetuumsa.bak' is available at the path '/data' + +USE [master] +GO + + +ALTER DATABASE perpetuumsa +SET SINGLE_USER WITH +ROLLBACK IMMEDIATE + +RESTORE DATABASE perpetuumsa FROM DISK = '/data/perpetuumsa.bak' WITH +MOVE 'perpetuumsa' TO '/data/psa.mdf', +MOVE 'perpetuumsa_log' TO '/data/psa_log.ldf', REPLACE + +ALTER DATABASE perpetuumsa SET MULTI_USER +GO \ No newline at end of file diff --git a/template/restore_migrated_DB.sql b/template/restore_migrated_DB.sql new file mode 100644 index 00000000..b35d0754 --- /dev/null +++ b/template/restore_migrated_DB.sql @@ -0,0 +1,16 @@ +USE [master] +GO + +IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'perpetuumsa') +BEGIN + ALTER DATABASE perpetuumsa SET SINGLE_USER WITH ROLLBACK IMMEDIATE +END +GO + +RESTORE DATABASE perpetuumsa FROM DISK = '/data/perpetuumsa_migrated.bak' WITH +MOVE 'perpetuumsa' TO '/data/psa.mdf', +MOVE 'perpetuumsa_log' TO '/data/psa_log.ldf', REPLACE +GO + +ALTER DATABASE perpetuumsa SET MULTI_USER +GO