diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..94b46351 --- /dev/null +++ b/.env.local @@ -0,0 +1,48 @@ +# 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 + +.PHONY: help up start stop down delete restart reset clean-cache log-asset log-db log-server test-unit test-integration + + 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..e00fe93e --- /dev/null +++ b/asset @@ -0,0 +1 @@ +Subproject commit e00fe93efeb3015789aeae94b66c281984177e2a diff --git a/compose.yml b/compose.yml new file mode 100644 index 00000000..627d3dfd --- /dev/null +++ b/compose.yml @@ -0,0 +1,115 @@ +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 + # 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 + ports: + - ${DB_PORT}:1433 + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=${DB_PASSWORD} + 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} + # .NET 8 DATAS: dynamically scales GC heaps to reduce idle memory usage + DOTNET_GCDynamicAdaptationMode: 1 + 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..e3675e2c --- /dev/null +++ b/db @@ -0,0 +1 @@ +Subproject commit e3675e2c6aaaa187141661e1f6b8556a5e606729 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/TESTING.md b/docs/codebase/TESTING.md index 5822d84c..ac9a3d21 100644 --- a/docs/codebase/TESTING.md +++ b/docs/codebase/TESTING.md @@ -31,6 +31,22 @@ set PERPETUUM_GAMEROOT=C:\PerpetuumServer\data dotnet test src/Perpetuum.Tests.Integration/Perpetuum.Tests.Integration.csproj -c Release -p:Platform=x64 ``` +### 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 +``` + +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, a full server run: ```bash 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..e140f1be --- /dev/null +++ b/script/migration.sh @@ -0,0 +1,112 @@ +#!/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/ +[ -d /custom-layers ] && cp -r /custom-layers/* /data/layers/ + +# Copy all patch-specific data/layers +for d in /migration/Patches/*/Server/data; do + [ -d "$d" ] && cp -r "$d"/* /data/ +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/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 2cf07914..afdaabbb 100644 --- a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs +++ b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs @@ -129,7 +129,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); @@ -137,6 +137,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 @@ -162,7 +163,7 @@ public void Init(string gameRoot) Logger.Info($"GC Latency mode: {GCSettings.LatencyMode}"); Logger.Info($"Vector is hardware accelerated: {Vector.IsHardwareAccelerated}"); - TransactionManager.ImplicitDistributedTransactions = true; + TransactionManager.ImplicitDistributedTransactions = 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/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/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/CompactPassabilityMaskTests.cs b/src/Perpetuum.Tests/Unit/CompactPassabilityMaskTests.cs new file mode 100644 index 00000000..da74d810 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/CompactPassabilityMaskTests.cs @@ -0,0 +1,58 @@ +using Perpetuum.Zones.Terrains; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class CompactPassabilityMaskTests + { + [Fact] + public void Bitmask_get_set_and_bounds() + { + var mask = new CompactPassabilityMask(64, 64); + + // Default is false (0) + Assert.False(mask.IsWalkable(0, 0)); + Assert.False(mask.IsWalkable(10, 10)); + + // Set some bits + mask.SetWalkable(0, 0, true); + mask.SetWalkable(15, 20, true); + mask.SetWalkable(31, 31, true); + mask.SetWalkable(32, 31, true); // cross 32-bit boundary + mask.SetWalkable(63, 63, true); + + Assert.True(mask.IsWalkable(0, 0)); + Assert.True(mask.IsWalkable(15, 20)); + Assert.True(mask.IsWalkable(31, 31)); + Assert.True(mask.IsWalkable(32, 31)); + Assert.True(mask.IsWalkable(63, 63)); + + // Unset a bit + mask.SetWalkable(15, 20, false); + Assert.False(mask.IsWalkable(15, 20)); + Assert.True(mask.IsWalkable(0, 0)); + + // Out of bounds + Assert.False(mask.IsWalkable(-1, 0)); + Assert.False(mask.IsWalkable(0, -1)); + Assert.False(mask.IsWalkable(64, 0)); + Assert.False(mask.IsWalkable(0, 64)); + } + + [Fact] + public void SetAll_fills_entire_grid() + { + var mask = new CompactPassabilityMask(100, 100); + mask.SetAll(true); + + Assert.True(mask.IsWalkable(0, 0)); + Assert.True(mask.IsWalkable(50, 50)); + Assert.True(mask.IsWalkable(99, 99)); + + mask.SetAll(false); + Assert.False(mask.IsWalkable(0, 0)); + Assert.False(mask.IsWalkable(50, 50)); + Assert.False(mask.IsWalkable(99, 99)); + } + } +} 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/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/HeightfieldMetadataTests.cs b/src/Perpetuum.Tests/Unit/HeightfieldMetadataTests.cs new file mode 100644 index 00000000..9f1d45fb --- /dev/null +++ b/src/Perpetuum.Tests/Unit/HeightfieldMetadataTests.cs @@ -0,0 +1,80 @@ +using Perpetuum.Zones.Terrains; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class HeightfieldMetadataTests + { + [Fact] + public void Chunk_bounds_and_ray_above_chunk_queries() + { + var rawData = new ushort[64 * 64]; + var altLayer = new AltitudeLayer(rawData, 64, 64); + + // Fill a chunk (0,0 to 15,15) with height 10.0 (raw: 10 * 32 = 320) + for (int y = 0; y < 16; y++) + { + for (int x = 0; x < 16; x++) + { + altLayer[x, y] = (ushort)(10 * 32); + } + } + + // Fill another chunk (16,0 to 31,15) with height 50.0 (raw: 50 * 32 = 1600) + for (int y = 0; y < 16; y++) + { + for (int x = 16; x < 32; x++) + { + altLayer[x, y] = (ushort)(50 * 32); + } + } + + var metadata = HeightfieldMetadata.ExtractFrom(altLayer, null, chunkSize: 16); + + Assert.Equal(4, metadata.ChunksX); + Assert.Equal(4, metadata.ChunksY); + + // Chunk (0,0) has max height 10.0 + metadata.GetChunkBounds(0, 0, out float min0, out float max0); + Assert.Equal(10.0f, min0); + Assert.Equal(10.0f, max0); + + // Chunk (1,0) has max height 50.0 + metadata.GetChunkBounds(1, 0, out float min1, out float max1); + Assert.Equal(50.0f, min1); + Assert.Equal(50.0f, max1); + + // Ray at Z=20 is strictly above chunk (0,0), but NOT above chunk (1,0) + Assert.True(metadata.CanRayPassAboveChunk(0, 0, rayMinZ: 20.0f)); + Assert.False(metadata.CanRayPassAboveChunk(1, 0, rayMinZ: 20.0f)); + + // Ray at Z=60 is above both + Assert.True(metadata.CanRayPassAboveChunk(0, 0, rayMinZ: 60.0f)); + Assert.True(metadata.CanRayPassAboveChunk(1, 0, rayMinZ: 60.0f)); + } + + [Fact] + public void Coordinates_mapping() + { + var metadata = new HeightfieldMetadata(2048, 2048, chunkSize: 16); + Assert.Equal(128, metadata.ChunksX); + Assert.Equal(128, metadata.ChunksY); + + metadata.GetChunkCoordinates(0, 0, out int cx0, out int cy0); + Assert.Equal(0, cx0); + Assert.Equal(0, cy0); + + metadata.GetChunkCoordinates(15, 15, out int cx1, out int cy1); + Assert.Equal(0, cx1); + Assert.Equal(0, cy1); + + metadata.GetChunkCoordinates(16, 32, out int cx2, out int cy2); + Assert.Equal(1, cx2); + Assert.Equal(2, cy2); + + metadata.GetChunkCoordinates(2047, 2047, out int cx3, out int cy3); + Assert.Equal(127, cx3); + Assert.Equal(127, cy3); + } + } +} diff --git a/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs b/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs new file mode 100644 index 00000000..f0186617 --- /dev/null +++ b/src/Perpetuum.Tests/Unit/LayerExtensionsTests.cs @@ -0,0 +1,46 @@ +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]); + } + } +} 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/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/SimdMathTests.cs b/src/Perpetuum.Tests/Unit/SimdMathTests.cs new file mode 100644 index 00000000..5ebad70d --- /dev/null +++ b/src/Perpetuum.Tests/Unit/SimdMathTests.cs @@ -0,0 +1,170 @@ +using Perpetuum.Simd; +using System; +using Xunit; + +namespace Perpetuum.Tests.Unit +{ + public class SimdMathTests + { + [Fact] + public void CalculateSquaredDistances2D_matches_scalar_math() + { + int count = 67; // Non-multiple of 16, 8, and 4 + float srcX = 150.5f; + float srcY = 200.25f; + + float[] targetXs = new float[count]; + float[] targetYs = new float[count]; + float[] simdDistSq = new float[count]; + float[] expectedDistSq = new float[count]; + + var rnd = new Random(42); + for (int i = 0; i < count; i++) + { + targetXs[i] = (float)(rnd.NextDouble() * 1000.0); + targetYs[i] = (float)(rnd.NextDouble() * 1000.0); + + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + expectedDistSq[i] = (dx * dx) + (dy * dy); + } + + SimdMath.CalculateSquaredDistances2D(srcX, srcY, targetXs, targetYs, simdDistSq); + + for (int i = 0; i < count; i++) + { + Assert.Equal(expectedDistSq[i], simdDistSq[i], precision: 3); + } + } + + [Fact] + public void CalculateSquaredDistances3D_matches_scalar_with_z_scaling() + { + int count = 45; + float srcX = 50.0f; + float srcY = 75.0f; + float srcZ = 120.0f; + + float[] targetXs = new float[count]; + float[] targetYs = new float[count]; + float[] targetZs = new float[count]; + float[] simdDistSq = new float[count]; + float[] expectedDistSq = new float[count]; + + var rnd = new Random(123); + for (int i = 0; i < count; i++) + { + targetXs[i] = (float)(rnd.NextDouble() * 500.0); + targetYs[i] = (float)(rnd.NextDouble() * 500.0); + targetZs[i] = (float)(rnd.NextDouble() * 200.0); + + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + float dz = (targetZs[i] - srcZ) / 4.0f; + expectedDistSq[i] = (dx * dx) + (dy * dy) + (dz * dz); + } + + SimdMath.CalculateSquaredDistances3D(srcX, srcY, srcZ, targetXs, targetYs, targetZs, simdDistSq); + + for (int i = 0; i < count; i++) + { + Assert.Equal(expectedDistSq[i], simdDistSq[i], precision: 3); + } + } + + [Fact] + public void FilterPointsInRange2D_filters_correctly() + { + int count = 50; + float srcX = 100.0f; + float srcY = 100.0f; + float range = 25.0f; + float rangeSq = range * range; + + float[] targetXs = new float[count]; + float[] targetYs = new float[count]; + bool[] simdResults = new bool[count]; + bool[] expectedResults = new bool[count]; + + var rnd = new Random(999); + for (int i = 0; i < count; i++) + { + // Place some points inside range (e.g. within 25) and some outside + targetXs[i] = srcX + (float)((rnd.NextDouble() - 0.5) * 60.0); + targetYs[i] = srcY + (float)((rnd.NextDouble() - 0.5) * 60.0); + + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + expectedResults[i] = ((dx * dx) + (dy * dy)) <= rangeSq; + } + + SimdMath.FilterPointsInRange2D(srcX, srcY, range, targetXs, targetYs, simdResults); + + for (int i = 0; i < count; i++) + { + Assert.Equal(expectedResults[i], simdResults[i]); + } + } + + [Fact] + public void FilterPositionsInRange3D_filters_correctly() + { + int count = 64; + float srcX = 200.0f; + float srcY = 200.0f; + float srcZ = 50.0f; + float range = 30.0f; + float rangeSq = range * range; + + float[] targetXs = new float[count]; + float[] targetYs = new float[count]; + float[] targetZs = new float[count]; + bool[] simdResults = new bool[count]; + bool[] expectedResults = new bool[count]; + + var rnd = new Random(777); + for (int i = 0; i < count; i++) + { + targetXs[i] = srcX + (float)((rnd.NextDouble() - 0.5) * 70.0); + targetYs[i] = srcY + (float)((rnd.NextDouble() - 0.5) * 70.0); + targetZs[i] = srcZ + (float)((rnd.NextDouble() - 0.5) * 100.0); + + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + float dz = (targetZs[i] - srcZ) / 4.0f; + expectedResults[i] = ((dx * dx) + (dy * dy) + (dz * dz)) <= rangeSq; + } + + SimdMath.FilterPositionsInRange3D(srcX, srcY, srcZ, range, targetXs, targetYs, targetZs, simdResults); + + for (int i = 0; i < count; i++) + { + Assert.Equal(expectedResults[i], simdResults[i]); + } + } + + [Fact] + public void EdgeCases_empty_and_small_arrays() + { + // Empty + SimdMath.CalculateSquaredDistances2D(0, 0, ReadOnlySpan.Empty, ReadOnlySpan.Empty, Span.Empty); + SimdMath.FilterPointsInRange2D(0, 0, 10, ReadOnlySpan.Empty, ReadOnlySpan.Empty, Span.Empty); + + // 1 element + float[] x1 = [10.0f]; + float[] y1 = [20.0f]; + float[] d1 = new float[1]; + SimdMath.CalculateSquaredDistances2D(0, 0, x1, y1, d1); + Assert.Equal(500.0f, d1[0]); + + // 3 elements (below Vector128 width of 4) + float[] x3 = [1.0f, 2.0f, 3.0f]; + float[] y3 = [0.0f, 0.0f, 0.0f]; + float[] d3 = new float[3]; + SimdMath.CalculateSquaredDistances2D(0, 0, x3, y3, d3); + Assert.Equal(1.0f, d3[0]); + Assert.Equal(4.0f, d3[1]); + Assert.Equal(9.0f, d3[2]); + } + } +} 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/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/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/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 9fd5157a..ab41f0d1 100644 --- a/src/Perpetuum/GlobalConfiguration.cs +++ b/src/Perpetuum/GlobalConfiguration.cs @@ -23,6 +23,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/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/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/Simd/SimdMath.cs b/src/Perpetuum/Simd/SimdMath.cs new file mode 100644 index 00000000..756ec45d --- /dev/null +++ b/src/Perpetuum/Simd/SimdMath.cs @@ -0,0 +1,375 @@ +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace Perpetuum.Simd +{ + /// + /// SIMD-accelerated math operations with AVX-512, AVX2, SSE2 and scalar fallbacks for .NET 8. + /// + public static class SimdMath + { + public static bool IsAvx512Supported => Vector512.IsHardwareAccelerated && Avx512F.IsSupported; + public static bool IsAvx2Supported => Vector256.IsHardwareAccelerated && Avx2.IsSupported; + public static bool IsVector128Supported => Vector128.IsHardwareAccelerated; + + /// + /// Calculates squared 2D distances from a source point (srcX, srcY) to an array of target points. + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void CalculateSquaredDistances2D( + float srcX, float srcY, + ReadOnlySpan targetXs, ReadOnlySpan targetYs, + Span destinationDistSq) + { + int count = Math.Min(targetXs.Length, Math.Min(targetYs.Length, destinationDistSq.Length)); + int i = 0; + + if (IsAvx512Supported && count >= 16) + { + var vSrcX = Vector512.Create(srcX); + var vSrcY = Vector512.Create(srcY); + + for (; i <= count - 16; i += 16) + { + var vX = Vector512.Create(targetXs.Slice(i, 16)); + var vY = Vector512.Create(targetYs.Slice(i, 16)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + distSq.CopyTo(destinationDistSq.Slice(i, 16)); + } + } + else if (IsAvx2Supported && count >= 8) + { + var vSrcX = Vector256.Create(srcX); + var vSrcY = Vector256.Create(srcY); + + for (; i <= count - 8; i += 8) + { + var vX = Vector256.Create(targetXs.Slice(i, 8)); + var vY = Vector256.Create(targetYs.Slice(i, 8)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + distSq.CopyTo(destinationDistSq.Slice(i, 8)); + } + } + else if (IsVector128Supported && count >= 4) + { + var vSrcX = Vector128.Create(srcX); + var vSrcY = Vector128.Create(srcY); + + for (; i <= count - 4; i += 4) + { + var vX = Vector128.Create(targetXs.Slice(i, 4)); + var vY = Vector128.Create(targetYs.Slice(i, 4)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + distSq.CopyTo(destinationDistSq.Slice(i, 4)); + } + } + + // Scalar remainder loop + for (; i < count; i++) + { + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + destinationDistSq[i] = (dx * dx) + (dy * dy); + } + } + + /// + /// Calculates squared 3D distances with Perpetuum Z-scaling (dz = (targetZ - srcZ) / 4.0). + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void CalculateSquaredDistances3D( + float srcX, float srcY, float srcZ, + ReadOnlySpan targetXs, ReadOnlySpan targetYs, ReadOnlySpan targetZs, + Span destinationDistSq) + { + int count = Math.Min(targetXs.Length, Math.Min(targetYs.Length, Math.Min(targetZs.Length, destinationDistSq.Length))); + int i = 0; + + const float zScale = 0.25f; // 1.0 / 4.0 + + if (IsAvx512Supported && count >= 16) + { + var vSrcX = Vector512.Create(srcX); + var vSrcY = Vector512.Create(srcY); + var vSrcZ = Vector512.Create(srcZ); + var vZScale = Vector512.Create(zScale); + + for (; i <= count - 16; i += 16) + { + var vX = Vector512.Create(targetXs.Slice(i, 16)); + var vY = Vector512.Create(targetYs.Slice(i, 16)); + var vZ = Vector512.Create(targetZs.Slice(i, 16)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + distSq.CopyTo(destinationDistSq.Slice(i, 16)); + } + } + else if (IsAvx2Supported && count >= 8) + { + var vSrcX = Vector256.Create(srcX); + var vSrcY = Vector256.Create(srcY); + var vSrcZ = Vector256.Create(srcZ); + var vZScale = Vector256.Create(zScale); + + for (; i <= count - 8; i += 8) + { + var vX = Vector256.Create(targetXs.Slice(i, 8)); + var vY = Vector256.Create(targetYs.Slice(i, 8)); + var vZ = Vector256.Create(targetZs.Slice(i, 8)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + distSq.CopyTo(destinationDistSq.Slice(i, 8)); + } + } + else if (IsVector128Supported && count >= 4) + { + var vSrcX = Vector128.Create(srcX); + var vSrcY = Vector128.Create(srcY); + var vSrcZ = Vector128.Create(srcZ); + var vZScale = Vector128.Create(zScale); + + for (; i <= count - 4; i += 4) + { + var vX = Vector128.Create(targetXs.Slice(i, 4)); + var vY = Vector128.Create(targetYs.Slice(i, 4)); + var vZ = Vector128.Create(targetZs.Slice(i, 4)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + distSq.CopyTo(destinationDistSq.Slice(i, 4)); + } + } + + // Scalar remainder loop + for (; i < count; i++) + { + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + float dz = (targetZs[i] - srcZ) * zScale; + destinationDistSq[i] = (dx * dx) + (dy * dy) + (dz * dz); + } + } + + /// + /// Filters target points that are within a specified 2D range from (srcX, srcY). + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void FilterPointsInRange2D( + float srcX, float srcY, float range, + ReadOnlySpan targetXs, ReadOnlySpan targetYs, + Span inRangeResults) + { + int count = Math.Min(targetXs.Length, Math.Min(targetYs.Length, inRangeResults.Length)); + float rangeSq = range * range; + int i = 0; + + if (IsAvx512Supported && count >= 16) + { + var vSrcX = Vector512.Create(srcX); + var vSrcY = Vector512.Create(srcY); + var vRangeSq = Vector512.Create(rangeSq); + + for (; i <= count - 16; i += 16) + { + var vX = Vector512.Create(targetXs.Slice(i, 16)); + var vY = Vector512.Create(targetYs.Slice(i, 16)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + var mask = Vector512.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 16; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + else if (IsAvx2Supported && count >= 8) + { + var vSrcX = Vector256.Create(srcX); + var vSrcY = Vector256.Create(srcY); + var vRangeSq = Vector256.Create(rangeSq); + + for (; i <= count - 8; i += 8) + { + var vX = Vector256.Create(targetXs.Slice(i, 8)); + var vY = Vector256.Create(targetYs.Slice(i, 8)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + var mask = Vector256.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 8; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + else if (IsVector128Supported && count >= 4) + { + var vSrcX = Vector128.Create(srcX); + var vSrcY = Vector128.Create(srcY); + var vRangeSq = Vector128.Create(rangeSq); + + for (; i <= count - 4; i += 4) + { + var vX = Vector128.Create(targetXs.Slice(i, 4)); + var vY = Vector128.Create(targetYs.Slice(i, 4)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var distSq = (dx * dx) + (dy * dy); + + var mask = Vector128.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 4; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + + for (; i < count; i++) + { + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + inRangeResults[i] = ((dx * dx) + (dy * dy)) <= rangeSq; + } + } + + /// + /// Filters target points that are within a specified 3D range with Perpetuum Z-scaling. + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void FilterPositionsInRange3D( + float srcX, float srcY, float srcZ, float range, + ReadOnlySpan targetXs, ReadOnlySpan targetYs, ReadOnlySpan targetZs, + Span inRangeResults) + { + int count = Math.Min(targetXs.Length, Math.Min(targetYs.Length, Math.Min(targetZs.Length, inRangeResults.Length))); + float rangeSq = range * range; + const float zScale = 0.25f; + int i = 0; + + if (IsAvx512Supported && count >= 16) + { + var vSrcX = Vector512.Create(srcX); + var vSrcY = Vector512.Create(srcY); + var vSrcZ = Vector512.Create(srcZ); + var vZScale = Vector512.Create(zScale); + var vRangeSq = Vector512.Create(rangeSq); + + for (; i <= count - 16; i += 16) + { + var vX = Vector512.Create(targetXs.Slice(i, 16)); + var vY = Vector512.Create(targetYs.Slice(i, 16)); + var vZ = Vector512.Create(targetZs.Slice(i, 16)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + var mask = Vector512.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 16; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + else if (IsAvx2Supported && count >= 8) + { + var vSrcX = Vector256.Create(srcX); + var vSrcY = Vector256.Create(srcY); + var vSrcZ = Vector256.Create(srcZ); + var vZScale = Vector256.Create(zScale); + var vRangeSq = Vector256.Create(rangeSq); + + for (; i <= count - 8; i += 8) + { + var vX = Vector256.Create(targetXs.Slice(i, 8)); + var vY = Vector256.Create(targetYs.Slice(i, 8)); + var vZ = Vector256.Create(targetZs.Slice(i, 8)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + var mask = Vector256.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 8; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + else if (IsVector128Supported && count >= 4) + { + var vSrcX = Vector128.Create(srcX); + var vSrcY = Vector128.Create(srcY); + var vSrcZ = Vector128.Create(srcZ); + var vZScale = Vector128.Create(zScale); + var vRangeSq = Vector128.Create(rangeSq); + + for (; i <= count - 4; i += 4) + { + var vX = Vector128.Create(targetXs.Slice(i, 4)); + var vY = Vector128.Create(targetYs.Slice(i, 4)); + var vZ = Vector128.Create(targetZs.Slice(i, 4)); + + var dx = vX - vSrcX; + var dy = vY - vSrcY; + var dz = (vZ - vSrcZ) * vZScale; + var distSq = (dx * dx) + (dy * dy) + (dz * dz); + + var mask = Vector128.LessThanOrEqual(distSq, vRangeSq); + + for (int j = 0; j < 4; j++) + { + inRangeResults[i + j] = mask.GetElement(j) != 0; + } + } + } + + for (; i < count; i++) + { + float dx = targetXs[i] - srcX; + float dy = targetYs[i] - srcY; + float dz = (targetZs[i] - srcZ) * zScale; + inRangeResults[i] = ((dx * dx) + (dy * dy) + (dz * dz)) <= rangeSq; + } + } + } +} 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 4a40afd9..9fd0e56d 100644 --- a/src/Perpetuum/Zones/IZone.cs +++ b/src/Perpetuum/Zones/IZone.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; -using System.Drawing; using Perpetuum.Accounting.Characters; using Perpetuum.Common.Loggers; using Perpetuum.Groups.Corporations; using Perpetuum.Log; using Perpetuum.Players; using Perpetuum.Services.Relics; -using Perpetuum.Services.Strongholds; using Perpetuum.Services.Weather; using Perpetuum.Units; using Perpetuum.Zones.Beams; @@ -20,6 +17,7 @@ using Perpetuum.Zones.Terrains.Materials.Plants; using Perpetuum.Zones.Terrains.Terraforming; using Perpetuum.Zones.ZoneEntityRepositories; +using SkiaSharp; namespace Perpetuum.Zones { @@ -29,7 +27,7 @@ public interface IZone bool IsLayerEditLocked { get; set; } int Id { get; } - Size Size { get; } + SKSizeI Size { get; } IEnumerable Units { get; } IEnumerable Players { 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 511b3958..d13e9e54 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) { @@ -378,9 +378,9 @@ private bool SelectPrimaryTarget() return validLocks.Length >= 1 && (stratSelector?.TryUseStrategy(smartCreature, validLocks) ?? 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. @@ -392,7 +392,7 @@ private List FindNewAttackPosition(Unit hostile, CancellationToken cancel priorityQueue.Enqueue(startNode); - HashSet closed = + HashSet closed = [ startNode.position ]; @@ -410,7 +410,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)) { @@ -445,7 +445,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); @@ -459,9 +459,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/HomingAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs index 63a2dcd1..20f64862 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs @@ -1,9 +1,7 @@ using Perpetuum.Modules; using Perpetuum.PathFinders; using Perpetuum.Zones.Movements; -using System; -using System.Collections.Generic; -using System.Linq; +using SkiaSharp; namespace Perpetuum.Zones.NpcSystem.AI { @@ -33,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/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 291df596..8d863526 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 { @@ -89,7 +90,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/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/Flocks/NormalFlock.cs b/src/Perpetuum/Zones/NpcSystem/Flocks/NormalFlock.cs index 8c7be994..dce8c825 100644 --- a/src/Perpetuum/Zones/NpcSystem/Flocks/NormalFlock.cs +++ b/src/Perpetuum/Zones/NpcSystem/Flocks/NormalFlock.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using Perpetuum.Timers; using Perpetuum.Units; using Perpetuum.Zones.NpcSystem.Presences; 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 6d90b6cf..47324473 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) { @@ -123,10 +120,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 d0495b81..a6b35c91 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; @@ -82,7 +79,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); @@ -97,7 +94,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); @@ -106,14 +103,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 9f053396..dcef1ad9 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/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..a57b0a34 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 System.Numerics; +using SkiaSharp; namespace Perpetuum.Zones { @@ -32,7 +32,7 @@ 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; } @@ -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/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/CompactPassabilityMask.cs b/src/Perpetuum/Zones/Terrains/CompactPassabilityMask.cs new file mode 100644 index 00000000..e9bdc537 --- /dev/null +++ b/src/Perpetuum/Zones/Terrains/CompactPassabilityMask.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Perpetuum.Zones.Terrains +{ + /// + /// A high-performance 1-bit per tile passability bitmask. + /// Provides cache-efficient (512 KB per 2048x2048 zone) walkability queries. + /// + public class CompactPassabilityMask + { + public int Width { get; } + public int Height { get; } + + private readonly uint[] _bits; + + public CompactPassabilityMask(int width, int height) + { + Width = width; + Height = height; + int totalBits = width * height; + _bits = new uint[(totalBits + 31) / 32]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsWalkable(int x, int y) + { + if ((uint)x >= (uint)Width || (uint)y >= (uint)Height) + return false; + + int bitIndex = (y * Width) + x; + int arrayIndex = bitIndex >> 5; + int bitOffset = bitIndex & 31; + + return (_bits[arrayIndex] & (1u << bitOffset)) != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetWalkable(int x, int y, bool walkable) + { + if ((uint)x >= (uint)Width || (uint)y >= (uint)Height) + return; + + int bitIndex = (y * Width) + x; + int arrayIndex = bitIndex >> 5; + int bitOffset = bitIndex & 31; + + if (walkable) + { + _bits[arrayIndex] |= (1u << bitOffset); + } + else + { + _bits[arrayIndex] &= ~(1u << bitOffset); + } + } + + public void SetAll(bool walkable) + { + uint value = walkable ? uint.MaxValue : 0u; + Array.Fill(_bits, value); + } + + public static CompactPassabilityMask ExtractFrom(ILayer blockingLayer, SlopeLayer slopeLayer, double slopeThreshold = 4.0) + { + var mask = new CompactPassabilityMask(blockingLayer.Width, blockingLayer.Height); + for (int y = 0; y < blockingLayer.Height; y++) + { + for (int x = 0; x < blockingLayer.Width; x++) + { + bool blocked = blockingLayer.GetValue(x, y).Height > 0; + bool slopeOk = slopeLayer.CheckSlope(x, y, slopeThreshold); + mask.SetWalkable(x, y, !blocked && slopeOk); + } + } + return mask; + } + } +} diff --git a/src/Perpetuum/Zones/Terrains/HeightfieldMetadata.cs b/src/Perpetuum/Zones/Terrains/HeightfieldMetadata.cs new file mode 100644 index 00000000..e21f7b80 --- /dev/null +++ b/src/Perpetuum/Zones/Terrains/HeightfieldMetadata.cs @@ -0,0 +1,129 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Perpetuum.Zones.Terrains +{ + /// + /// Pre-extracted hierarchical chunk bounding metadata from terrain layers (altitude and blocking) + /// to accelerate spatial queries, Line-of-Sight (LOS) raycasting, and obstacle checks. + /// + public class HeightfieldMetadata + { + public const int DefaultChunkSize = 16; + + public int Width { get; } + public int Height { get; } + public int ChunkSize { get; } + public int ChunksX { get; } + public int ChunksY { get; } + + private readonly float[] _minHeights; + private readonly float[] _maxHeights; + + public HeightfieldMetadata(int width, int height, int chunkSize = DefaultChunkSize) + { + Width = width; + Height = height; + ChunkSize = Math.Max(1, chunkSize); + ChunksX = (width + ChunkSize - 1) / ChunkSize; + ChunksY = (height + ChunkSize - 1) / ChunkSize; + + _minHeights = new float[ChunksX * ChunksY]; + _maxHeights = new float[ChunksX * ChunksY]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetChunkIndex(int chunkX, int chunkY) => (chunkY * ChunksX) + chunkX; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetChunkCoordinates(int tileX, int tileY, out int chunkX, out int chunkY) + { + chunkX = Math.Clamp(tileX / ChunkSize, 0, ChunksX - 1); + chunkY = Math.Clamp(tileY / ChunkSize, 0, ChunksY - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetChunkBounds(int chunkX, int chunkY, out float minH, out float maxH) + { + if (chunkX < 0 || chunkX >= ChunksX || chunkY < 0 || chunkY >= ChunksY) + { + minH = float.MinValue; + maxH = float.MaxValue; + return; + } + + int idx = GetChunkIndex(chunkX, chunkY); + minH = _minHeights[idx]; + maxH = _maxHeights[idx]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanRayPassAboveChunk(int chunkX, int chunkY, float rayMinZ) + { + if (chunkX < 0 || chunkX >= ChunksX || chunkY < 0 || chunkY >= ChunksY) + return false; + + int idx = GetChunkIndex(chunkX, chunkY); + return rayMinZ > _maxHeights[idx]; + } + + /// + /// Extracts and bakes chunk min/max metadata from an AltitudeLayer and optional Blocking Layer. + /// + public static HeightfieldMetadata ExtractFrom(AltitudeLayer altitudeLayer, ILayer blockingLayer = null, int chunkSize = DefaultChunkSize) + { + var metadata = new HeightfieldMetadata(altitudeLayer.Width, altitudeLayer.Height, chunkSize); + metadata.RecomputeAll(altitudeLayer, blockingLayer); + return metadata; + } + + public void RecomputeAll(AltitudeLayer altitudeLayer, ILayer blockingLayer = null) + { + for (int cy = 0; cy < ChunksY; cy++) + { + for (int cx = 0; cx < ChunksX; cx++) + { + RecomputeChunk(cx, cy, altitudeLayer, blockingLayer); + } + } + } + + public void RecomputeChunk(int chunkX, int chunkY, AltitudeLayer altitudeLayer, ILayer blockingLayer = null) + { + int startX = chunkX * ChunkSize; + int startY = chunkY * ChunkSize; + int endX = Math.Min(startX + ChunkSize, Width); + int endY = Math.Min(startY + ChunkSize, Height); + + float min = float.MaxValue; + float max = float.MinValue; + + for (int y = startY; y < endY; y++) + { + for (int x = startX; x < endX; x++) + { + float alt = (float)altitudeLayer.GetAltitudeAsDouble(x, y); + float blockHeight = 0; + if (blockingLayer != null) + { + blockHeight = blockingLayer.GetValue(x, y).Height; + } + + float totalHeight = alt + blockHeight; + if (totalHeight < min) min = totalHeight; + if (totalHeight > max) max = totalHeight; + } + } + + if (min > max) + { + min = 0; + max = 0; + } + + int idx = GetChunkIndex(chunkX, chunkY); + _minHeights[idx] = min; + _maxHeights[idx] = max; + } + } +} 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/LayerExtensions.cs b/src/Perpetuum/Zones/Terrains/LayerExtensions.cs index f5b01f01..6f4dc74e 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); } 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/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 099e66ab..7c557dad 100644 --- a/src/Perpetuum/Zones/Zone.cs +++ b/src/Perpetuum/Zones/Zone.cs @@ -31,8 +31,8 @@ using Perpetuum.Zones.ZoneEntityRepositories; using System.Collections.Immutable; using System.Diagnostics; -using System.Drawing; using System.Net.Sockets; +using SkiaSharp; namespace Perpetuum.Zones { @@ -43,7 +43,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; } @@ -326,6 +326,7 @@ public Player GetPlayer(long eid) } private readonly ShiftedConsumerTimer _updateUnitsTimer = new ShiftedConsumerTimer(500); + private readonly IntervalTimer _idleUpdateTimer = new IntervalTimer(1000); private Action _updateProfiler; @@ -344,6 +345,22 @@ public override void Update(TimeSpan time) { UpdateSessions(time); + // Throttle unit physics, AI, and visibility processing when no players are in the zone + if (_players.IsEmpty) + { + _idleUpdateTimer.Update(time); + if (!_idleUpdateTimer.Passed) + { + RiftManager?.Update(time); + RelicManager?.Update(time); + MiningLogHandler.Update(time); + HarvestLogHandler.Update(time); + return; + } + + _idleUpdateTimer.Reset(); + } + _updateUnitsTimer.Update(time).IsPassed(ProcessUpdatedUnits); UpdateUnits(time); 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 fe9e047b..3620c7cb 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.Security.Cryptography; namespace Perpetuum.Zones @@ -64,23 +64,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 +91,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 +108,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 +138,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/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