41 Commits
Author SHA1 Message Date
linmeideli 6c83b127d3 restore Dockerfile
CI / build (push) Canceled after 0s
CI / publish-nightly (push) Canceled after 0s
CI / docker (map[docker:linux/amd64 rid:amd64]) (push) Canceled after 0s
CI / docker (map[docker:linux/arm/v7 rid:arm]) (push) Canceled after 0s
CI / docker (map[docker:linux/arm64 rid:arm64]) (push) Canceled after 0s
2026-09-25 19:39:10 +08:00
linmeideli b9e40852f4 clean up and bump 2026-09-25 19:33:50 +08:00
linmeideli f3043cfc5e ci: restore ci.N prerelease suffix and drop workflow self-trigger 2026-09-25 19:06:44 +08:00
linmeideli b675d07d2e ci: package version comes from project file only 2026-09-25 18:46:26 +08:00
linmeideli bcd18b7d85 ci: use nuget.org trusted publishing (OIDC) instead of static key 2026-09-25 18:17:43 +08:00
Delusional Syndrome 2651a93230 Update README.md
CI / build (push) Canceled after 0s
nuget / publish (push) Canceled after 0s
CI / publish-nightly (push) Canceled after 0s
CI / docker (map[docker:linux/amd64 rid:amd64]) (push) Canceled after 0s
CI / docker (map[docker:linux/arm/v7 rid:arm]) (push) Canceled after 0s
CI / docker (map[docker:linux/arm64 rid:arm64]) (push) Canceled after 0s
2026-09-25 13:01:51 +08:00
linmeideli 211c79f9d0 fix nuget version numbering to follow CI run number 2026-09-25 12:49:38 +08:00
Delusional Syndrome dc0f115948 Fix project name and add description to README
Updated the README to correct the project name and provide a brief description.
2026-09-25 12:38:49 +08:00
linmeideli 2d3a5ca800 fix docker remain issue 2026-09-25 12:32:20 +08:00
linmeideli 1ced63af8d fix 2026-09-25 12:26:02 +08:00
linmeideli ca54e6f2ed Update Nuget 2026-09-25 11:52:26 +08:00
linmeideli a2daa01993 both revert + docker fix 2026-09-25 10:26:21 +08:00
linmeideli edfff9ae19 Deleted Niko and Relay auth 2026-09-25 10:20:17 +08:00
linmeideli 8767bcbd4d Change IpGeolocationService to english 2026-09-25 10:04:44 +08:00
linmeideli 1d7252aa36 Merge branch 'main' of https://github.com/Empostor/Empostor 2026-09-25 09:59:49 +08:00
linmeideli 90e995261d enhance docker 2026-09-25 09:59:38 +08:00
FangkuaiYa cc8085f138 Fix build 2026-09-12 02:42:37 +08:00
FangkuaiYa 34e6751d33 Some support and improvements 2026-09-12 02:33:43 +08:00
linmeideli f72a8aa59f Update 2026-09-06 14:00:49 +08:00
linmeideli a5dd48a5ed matching Impostor 2026-09-06 11:57:30 +08:00
linmeideli 490a59be62 Improve file formal and clear program self 2026-08-29 16:28:01 +08:00
linmeideli e1285972d7 update theme 2026-08-29 15:59:30 +08:00
linmeideli d761f31fa3 Reapply "Add fault tolerance to GameData messages"
This reverts commit 4ca2efc6cd.
2026-08-29 15:45:32 +08:00
FangkuaiYa 4ca2efc6cd Revert "Add fault tolerance to GameData messages"
This reverts commit 7c8c629ea5.
2026-08-22 14:33:47 +08:00
Christina e747f8d71a Merge pull request #7 from Nb1X/docs/add-readme
docs: add project README
2026-08-20 23:47:58 +08:00
Christina 6a21cde5ac Update README with credits and QQ group
Added credits section and QQ group link to README.
2026-08-20 22:16:49 +08:00
Christina 9fcc718f63 Enhance README with new features and links
Updated README to include Admin Panel feature and new Discord link.
2026-08-20 22:01:58 +08:00
Christina f3a7463949 Merge pull request #2 from Empostor/gamedata-enhance
Add fault tolerance to GameData messages
2026-08-20 21:58:44 +08:00
Nb1X 5c650e8970 Added a README.md
Added README.md with project details and setup instructions.
2026-08-20 14:09:51 +02:00
FangkuaiYa 43f8281486 Fix playerlog 2026-08-20 15:17:46 +08:00
linmeideli 07f81c5d75 Merge branch 'main' of https://github.com/Empostor/Empostor 2026-08-20 12:14:28 +08:00
linmeideli f0cb36f118 merge 2026-08-20 12:14:26 +08:00
Christina 2ba175d16c Merge pull request #6 from Empostor/adminpanel-refect
Adminpanel refect
2026-08-20 10:19:21 +08:00
Christina 068cbcabc4 Merge pull request #5 from Empostor/v18.0-support
filter + upstream support
2026-08-20 09:15:47 +08:00
linmeideli 6498752430 filter + upstream support 2026-08-20 09:14:30 +08:00
linmeideli 031b6c1388 Update 2026-08-19 10:52:47 +08:00
Fang-Kuai 3efbb19785 Merge pull request #3 from XtraCube/main
Support new Starlight API Spec
2026-08-18 11:12:43 +08:00
Sameer Umar 989c1b5ff2 Support new Starlight API Spec 2026-08-17 23:03:50 -04:00
linmeideli f8eeeaae8b Enhanced Ban 2026-08-18 00:11:41 +08:00
linmeideli 40bbc2027b Bump 2026-08-17 23:42:49 +08:00
linmeideli 7c8c629ea5 Add fault tolerance to GameData messages 2026-08-17 12:12:40 +08:00
299 changed files with 67319 additions and 11746 deletions

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
name: nuget
on:
push:
branches: [main]
paths:
- "src/Empostor.Api/**"
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
# Package version = <VersionPrefix>-ci.<CI run_number + 500>, mirroring
# the nightly build numbering. The floor (max already-published ci.N + 1)
# keeps versions monotonic; push is skipped by --skip-duplicate if the
# exact version already exists.
- name: Resolve version
id: ver
run: |
set -euo pipefail
PROJ=src/Empostor.Api/Empostor.Api.csproj
BASE=$(sed -n 's/.*<VersionPrefix>\([^<]*\)<\/VersionPrefix>.*/\1/p' "$PROJ" | head -1)
CI_RUN=$(curl -sf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/actions/workflows/ci.yml/runs?per_page=1" \
| jq -r '.workflow_runs[0].run_number')
N=$(( CI_RUN + 500 ))
MAXN=$(curl -sf "https://api.nuget.org/v3-flatcontainer/empostor.api/index.json" \
| jq -r '.versions[]' | grep -oE 'ci\.[0-9]+$' | grep -oE '[0-9]+$' | sort -n | tail -1 || true)
MAXN=${MAXN:-0}
if [ "$N" -le "$MAXN" ]; then N=$((MAXN + 1)); fi
echo "suffix=ci.${N}" >> "$GITHUB_OUTPUT"
echo "version=${BASE}-ci.${N}" >> "$GITHUB_OUTPUT"
echo "Publishing version: ${BASE}-ci.${N}"
- name: Pack
run: dotnet pack src/Empostor.Api/Empostor.Api.csproj -c Release -p:VersionSuffix=${{ steps.ver.outputs.suffix }} -p:GeneratePackageOnBuild=false -o artifacts --nologo
# Trusted publishing: exchanges the GitHub OIDC token for a short-lived
# nuget.org API key tied to the policy (no secret needed).
- name: NuGet login (trusted publishing)
uses: NuGet/login@v1
id: login
with:
user: ELinmei
- name: Push to nuget.org
run: |
set -euo pipefail
ls -la artifacts
dotnet nuget push "artifacts/Empostor.Api.*.nupkg" \
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
dotnet nuget push "artifacts/Empostor.Api.*.snupkg" \
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate || true
+6 -4
View File
@@ -25,8 +25,7 @@ WORKDIR /source
COPY src/Empostor.Server/Empostor.Server.csproj ./src/Empostor.Server/
COPY src/Empostor.Api/Empostor.Api.csproj ./src/Empostor.Api/
COPY src/Empostor.Api.Innersloth.Generator/Empostor.Api.Innersloth.Generator.csproj ./src/Empostor.Api.Innersloth.Generator/
COPY src/NextFast.Hazel-New/Next.Hazel/Next.Hazel.csproj ./src/NextFast.Hazel-New/Next.Hazel/
COPY src/NextFast.Hazel-New/Next.Hazel.Abstractions/Next.Hazel.Abstractions.csproj ./src/NextFast.Hazel-New/Next.Hazel.Abstractions/
COPY src/Empostor.Plugins.PlayerLog/Empostor.Plugins.PlayerLog.csproj ./src/Empostor.Plugins.PlayerLog/
COPY src/Directory.Build.props ./src/
RUN case "$TARGETARCH" in \
@@ -76,11 +75,14 @@ RUN mkdir -p /app/plugins /app/libraries /app/Languages /app/Log /app/Data /app/
ENV ASPNETCORE_URLS=http://0.0.0.0:80
ENV EMPOSTOR_Server__PublicIp=0.0.0.0
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV DOTNET_RUNNING_IN_CONTAINER=1
EXPOSE 80/tcp
EXPOSE 22023/tcp
# Delta UDP port pool (no static UDP port when delta mode is enabled).
EXPOSE 22024-22099/udp
# Game / matchmaker UDP (Server.ListenPort) — always bound, even in delta mode.
EXPOSE 22023/udp
# Delta UDP port pool (Server.DeltaPortStart/End). Keep in sync with config.json.
EXPOSE 22024-42024/udp
VOLUME ["/app/config", "/app/plugins", "/app/libraries", "/app/Languages", "/app/Log", "/app/Data", "/app/marketplace"]
+65
View File
@@ -0,0 +1,65 @@
# Empostor
Empostor is an open-source private server implementation for Among Us.
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=flat&logo=discord&logoColor=white)](https://dsc.gg/empostor)
[![QQ](https://img.shields.io/badge/QQ-Group-black?style=flat-square)](https://qm.qq.com/q/GeX3Q0Ft0k)
[![GitHub license](https://badgen.net/github/license/Empostor/Empostor)](https://github.com/Empostor/Empostor/blob/main/LICENSE)
[![GitHub latest commit](https://badgen.net/github/last-commit/Empostor/Empostor)](https://github.com/Empostor/Empostor/commit/)
[![GitHub all releases](https://img.shields.io/github/downloads/Empostor/Empostor/total.svg)](https://github.com/Empostor/Empostor/releases/)
[![GitHub contributors](https://badgen.net/github/contributors/Empostor/Empostor)](https://github.com/Empostor/Empostor/graphs/contributors/)
[![GitHub total-pull-requests](https://badgen.net/github/prs/Empostor/Empostor)](https://github.com/Empostor/Empostor/pull/)
## Features
- FriendCode support (authentication)
- Dynamic ports
- Admin Panel (dashboard)
- Plugin support
- Server-side anticheat
## Getting Started
To run the server, you need:
- [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
Download the build you'd like from the [releases](https://github.com/Empostor/Empostor/releases). You most likely want the x64 build. Use the arm64 build if you are running Empostor on a Raspberry Pi, another ARM-based SBC, or an ARM VPS.
Unzip the file and go to config.json. To make your server playable for other devices, replace the "PublicIp" field with your actual public IP address.
It is also recommended to [set up a Reverse Proxy](https://empostor.github.io/get-started/reverse-proxy#use-a-reverse-proxy) (for HTTPS connections).
## Client Setup
### Windows
1. Go [here](https://empostor.github.io/empostor) and enter your server's IP/domain, port and name. Press "Download server file"
2. Press Win + R and enter this (leave the quotation marks):
```cmd
"%userprofile%\AppData\LocalLow\Innersloth\Among Us"
```
3. Put the new regionInfo.json file you downloaded in the folder. Overwrite the existing file in the folder.
4. Launch Among Us. If everything worked, your server should appear in the regions list!
### Android/iOS
1. Launch Among Us. When you reach the main menu, close the app.
2. Go [here](https://empostor.github.io/empostor) and enter your server's IP/domain, port and name.
3. Scroll until you see Instructions and press the Android or Apple logo.
4. Press "Open in Among Us". Among Us should open.
5. Your server should appear in the regions list!
## Contributing
Please read the [contributing guidelines](https://github.com/Empostor/Empostor/blob/main/CONTRIBUTING.md).
You're welcome to open a pull request/issue!
## Documentation
The documentation is available [here](https://empostor.github.io)!
## License
This project is licensed under the [GPL-v3.0 License](https://github.com/Empostor/Empostor/blob/main/LICENSE).
## Credits
- [Impostor](https://github.com/Impostor/Impostor)
- [Next-Impostor](https://github.com/BunchHanpiDev/Next-Impostor)
- [NextFast.Hazel](https://github.com/Next-Fast/NextFast.Hazel)
- [Reactor.Impostor](https://github.com/NuclearPowered/Reactor.Impostor)
+16 -6
View File
@@ -7,18 +7,24 @@ services:
restart: unless-stopped
ports:
# HTTP API / admin panel (TCP)
# HTTP API / Admin panel (TCP)
- "22023:22023/tcp"
# Delta UDP port pool. With DeltaPortStart/End enabled, the server does
# NOT bind a static UDP port at startup — each player gets a unique port
# from this range, and when only one port is left it is shared by all
# new players (IP-based matching). Adjust to match config.json.
# Game / Matchmaker UDP — REQUIRED. The matchmaker always binds this UDP
# port (Server.ListenPort) so players can connect; without it the server
# is unreachable even in delta mode.
- "22023:22023/udp"
# Delta UDP port pool. With DeltaPortStart/End enabled, each player is
# assigned a unique UDP port from this range (Server.DeltaPortStart/End).
# Must stay in sync with config.json.
- "22024-42024:22024-42024/udp"
# Uncomment to expose admin HTTP panel:
# - "80:80/tcp"
volumes:
# Server config (can be read-only if you only edit via file)
# Server config. NOTE: if ./config.json does NOT exist on the host, Docker
# creates an EMPTY DIRECTORY at that path and mounts it over the baked-in
# /app/config.json, which breaks startup. Create the file first (or remove
# this line to use the image's default config) before running.
- ./config.json:/app/config.json:ro
# Writable runtime data
- empostor_plugins:/app/plugins
@@ -31,8 +37,12 @@ services:
- ./discord_webhook.json:/app/discord_webhook.json
environment:
- DOTNET_RUNNING_IN_CONTAINER=1
- ASPNETCORE_URLS=http://0.0.0.0:80
# PublicIp MUST be your real public IP / domain, or remote players can't
# connect. 0.0.0.0 / 127.0.0.1 will NOT work for anyone but localhost.
- EMPOSTOR_Server__PublicIp=0.0.0.0
- EMPOSTOR_Server__ListenIp=0.0.0.0
volumes:
empostor_plugins:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+78 -20
View File
@@ -9,11 +9,6 @@
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugin.Chat.dll"
},
{
"version": "1.1.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugin.Chat1.1.0.dll"
}
]
},
@@ -40,16 +35,6 @@
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome.dll"
},
{
"version": "1.1.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome1.1.0.dll"
},
{
"version": "1.2.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome1.2.0.dll"
}
]
},
@@ -89,11 +74,6 @@
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Titles.dll"
},
{
"version": "1.1.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Titles1.1.0.dll"
}
]
},
@@ -174,5 +154,83 @@
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Reactor.Empostor.dll"
}
]
},
{
"id": "cn.hayashiume.chatfilter",
"name": "Chat Filter",
"description": "Filter the message depend on the banwords",
"author": "HayashiUme",
"versions": [
{
"version": "2.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.ChatFilter.dll"
}
]
},
{
"id": "cn.hayashiume.discordwebhook",
"name": "Discord Webhook",
"description": "Send lobby and admin message to dicord by webhook",
"author": "HayashiUme",
"versions": [
{
"version": "2.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.DiscordWebhook.dll"
}
]
},
{
"id": "cn.hayashiume.playerstats",
"name": "Player Stats",
"description": "Stat player logs with #stats command",
"author": "HayashiUme",
"versions": [
{
"version": "2.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.PlayerStats.dll"
}
]
},
{
"id": "gg.empostor.playerlog",
"name": "Player Log",
"description": "Logs all player activity (chat, kills, meetings, joins, leaves) and provides an admin panel to view, search, and export logs.",
"author": "Empostor",
"versions": [
{
"version": "2.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.PlayerLog.dll"
}
]
},
{
"id": "gg.empostor.monitor",
"name": "Monitor",
"description": "Provides /api/monitor/status and /api/monitor/health endpoints for server health checks and uptime monitoring.",
"author": "Empostor",
"versions": [
{
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.Monitor.dll"
}
]
},
{
"id": "gg.empostor.privacy",
"name": "Privacy Policy",
"description": "Serves a customizable privacy policy page at /privacy with an admin panel editor.",
"author": "Empostor",
"versions": [
{
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.Privacy.dll"
}
]
}
]
@@ -85,6 +85,14 @@ public sealed class SourceGenerator : IIncrementalGenerator
enumGenerator.Generate("SystemTypes", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("Language", sourceName: "SupportedLangs");
enumGenerator.Generate("TaskTypes");
enumGenerator.Generate("BoolOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("ByteOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("CategorizedOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatArrayOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32ArrayOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32OptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("UInt32OptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("RpcCalls", "Empostor.Api.Net.Inner", underlyingType: CSharpEnumUnderlyingType.Byte);
+55
View File
@@ -0,0 +1,55 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
namespace Empostor.Api.Admin;
public delegate ValueTask<AdminActionResult> AdminAction(AdminActionContext context);
/// <summary>
/// The result of an admin action. Tells the renderer what to do next.
/// </summary>
/// <param name="Success">Whether the action completed successfully.</param>
/// <param name="Message">An optional toast message shown to the operator.</param>
/// <param name="Refresh">When true, the client re-fetches the panel to reflect new state.</param>
public sealed record AdminActionResult(bool Success, string? Message = null, bool Refresh = true)
{
public static AdminActionResult Ok(string? message = null, bool refresh = true) => new(true, message, refresh);
public static AdminActionResult Fail(string message) => new(false, message, true);
}
public sealed class AdminActionContext
{
public AdminActionContext(
string extensionId,
string actionId,
string? value,
JsonElement? payload,
IServiceProvider services)
{
ExtensionId = extensionId;
ActionId = actionId;
Value = value;
Payload = payload;
Services = services;
}
/// <summary>The extension that owns the invoked action.</summary>
public string ExtensionId { get; }
/// <summary>The action id that was invoked.</summary>
public string ActionId { get; }
/// <summary>
/// The new value submitted by the widget (textbox text, toggle on/off, select value,
/// number value, chip value). <c>null</c> for plain button clicks.
/// </summary>
public string? Value { get; }
/// <summary>The full JSON body of the request, if the widget sent structured data.</summary>
public JsonElement? Payload { get; }
/// <summary>The DI container, for resolving services the handler may need.</summary>
public IServiceProvider Services { get; }
}
+109
View File
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
namespace Empostor.Api.Admin;
/// <summary>
/// Fluent builder used by an <see cref="IAdminExtension"/> to declare its admin-panel content.
/// Register* methods add declarative widgets; widgets that carry a callback get a stable,
/// auto-assigned action id that the client sends back on interaction.
/// </summary>
public sealed class AdminPanelBuilder
{
private readonly Dictionary<string, AdminAction> _actions = new(StringComparer.Ordinal);
private int _actionCounter;
public AdminPanelBuilder(string extensionId, string title, string icon, string? section)
{
ExtensionId = extensionId;
Title = title;
Icon = icon;
Section = string.IsNullOrWhiteSpace(section) ? "Plugins" : section;
}
public string ExtensionId { get; }
public string Title { get; }
public string Icon { get; }
public string Section { get; }
public List<AdminWidget> Widgets { get; } = new();
public IReadOnlyDictionary<string, AdminAction> Actions => _actions;
public AdminPanelBuilder RegisterButton(Action<AdminButton> configure) => Register(configure);
public AdminPanelBuilder RegisterText(string content, string tone = "default", bool monospace = false)
{
Widgets.Add(new AdminText { Content = content, Tone = tone, Monospace = monospace });
return this;
}
public AdminPanelBuilder RegisterText(Action<AdminText> configure) => Register(configure);
public AdminPanelBuilder RegisterBlock(Action<AdminBlock> configure) => Register(configure);
public AdminPanelBuilder RegisterTextbox(Action<AdminTextbox> configure) => Register(configure);
public AdminPanelBuilder RegisterToggle(Action<AdminToggle> configure) => Register(configure);
public AdminPanelBuilder RegisterSelect(Action<AdminSelect> configure) => Register(configure);
public AdminPanelBuilder RegisterNumber(Action<AdminNumber> configure) => Register(configure);
public AdminPanelBuilder RegisterTable(Action<AdminTable> configure) => Register(configure);
public AdminPanelBuilder RegisterChips(Action<AdminChips> configure) => Register(configure);
public AdminPanelBuilder RegisterDivider()
{
Widgets.Add(new AdminDivider());
return this;
}
private AdminPanelBuilder Register<T>(Action<T> configure)
where T : AdminWidget, new()
{
var widget = new T();
configure(widget);
Bind(widget);
Widgets.Add(widget);
return this;
}
private void Bind(AdminWidget widget)
{
if (widget.Handler != null)
{
widget.Action = NextAction();
_actions[widget.Action] = widget.Handler;
}
if (widget is AdminChips chips)
{
if (chips.AddHandler != null)
{
chips.AddAction = NextAction();
_actions[chips.AddAction] = chips.AddHandler;
}
if (chips.RemoveHandler != null)
{
chips.RemoveAction = NextAction();
_actions[chips.RemoveAction] = chips.RemoveHandler;
}
}
if (widget is AdminBlock block)
{
foreach (var child in block.Children)
{
Bind(child);
}
}
}
private string NextAction() => $"{ExtensionId}.a{_actionCounter++}";
}
+74
View File
@@ -0,0 +1,74 @@
using System.Collections.Generic;
namespace Empostor.Api.Admin;
/// <summary>
/// A theme that reskins the admin panel, in the spirit of VuePress's "theme as plugin".
/// A theme can also be shipped as a folder under <c>Pages/themes/{Id}/theme.json</c>.
/// </summary>
public interface IAdminTheme
{
/// <summary>Unique id. Also the folder name for disk-based themes.</summary>
string Id { get; }
/// <summary>Human-readable name.</summary>
string Name { get; }
/// <summary>Id of a parent theme whose tokens/css/layout this theme inherits.</summary>
string? Extends => null;
/// <summary>Build the theme definition (tokens, palette, css, layout).</summary>
AdminThemeDefinition Define();
}
/// <summary>
/// Declarative description of a theme's styling layers.
/// </summary>
public sealed class AdminThemeDefinition
{
/// <summary>
/// Design tokens for the default (light) appearance, keyed by CSS custom-property name
/// (e.g. <c>--bg</c>, <c>--a</c>). Use the names in <see cref="AdminThemeTokens"/>.
/// </summary>
public Dictionary<string, string> Tokens { get; set; } = new();
/// <summary>
/// Dark-appearance overrides applied under <c>[data-theme="dark"]</c>. When omitted,
/// the framework keeps the light tokens for both modes.
/// </summary>
public Dictionary<string, string>? DarkTokens { get; set; }
/// <summary>Raw CSS appended after the tokens (palette/component styles, animations).</summary>
public string? CustomCss { get; set; }
/// <summary>
/// Optional layout template. Slots are <c>{{slot:header}}</c>, <c>{{slot:nav}}</c>,
/// <c>{{slot:content}}</c> and <c>{{slot:footer}}</c>. When omitted the built-in layout is used.
/// </summary>
public string? Layout { get; set; }
/// <summary>Named component aliases (component name → HTML fragment) usable in the layout.</summary>
public Dictionary<string, string> Components { get; set; } = new();
}
/// <summary>
/// The standard set of design-token names the admin panel consumes. Themes should fill
/// these keys so their palette applies to every built-in control.
/// </summary>
public static class AdminThemeTokens
{
public const string Background = "--bg";
public const string Surface = "--s";
public const string Border = "--b";
public const string Text = "--t";
public const string Muted = "--m";
public const string Accent = "--a";
public const string Success = "--g";
public const string Warning = "--y";
public const string Danger = "--r";
public const string Purple = "--p";
public const string Orange = "--o";
public const string Field = "--field";
public const string Hover = "--hover";
public const string OnAccent = "--on-a";
}
+177
View File
@@ -0,0 +1,177 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Admin;
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(AdminButton), "button")]
[JsonDerivedType(typeof(AdminText), "text")]
[JsonDerivedType(typeof(AdminBlock), "block")]
[JsonDerivedType(typeof(AdminTextbox), "textbox")]
[JsonDerivedType(typeof(AdminToggle), "toggle")]
[JsonDerivedType(typeof(AdminSelect), "select")]
[JsonDerivedType(typeof(AdminNumber), "number")]
[JsonDerivedType(typeof(AdminTable), "table")]
[JsonDerivedType(typeof(AdminChips), "chips")]
[JsonDerivedType(typeof(AdminDivider), "divider")]
public abstract class AdminWidget
{
public string? Action { get; set; }
[JsonIgnore]
public AdminAction? Handler { get; set; }
}
public sealed class AdminButton : AdminWidget
{
public string Label { get; set; } = string.Empty;
public string? Icon { get; set; }
public string Style { get; set; } = "primary";
public AdminButton OnClick(AdminAction handler)
{
Handler = handler;
return this;
}
}
public sealed class AdminText : AdminWidget
{
public string Content { get; set; } = string.Empty;
public string Tone { get; set; } = "default";
public bool Monospace { get; set; }
}
public sealed class AdminBlock : AdminWidget
{
public string? Title { get; set; }
public string? Description { get; set; }
public List<AdminWidget> Children { get; set; } = new();
}
public sealed class AdminTextbox : AdminWidget
{
public string Label { get; set; } = string.Empty;
public string? Value { get; set; }
public string? Placeholder { get; set; }
public bool Secret { get; set; }
public bool Multiline { get; set; }
public bool SubmitOnChange { get; set; }
public AdminTextbox OnSubmit(AdminAction handler)
{
Handler = handler;
return this;
}
}
public sealed class AdminToggle : AdminWidget
{
public string Label { get; set; } = string.Empty;
public bool Value { get; set; }
public AdminToggle OnChange(AdminAction handler)
{
Handler = handler;
return this;
}
}
public sealed class AdminSelect : AdminWidget
{
public string Label { get; set; } = string.Empty;
public List<AdminOption> Options { get; set; } = new();
public string? Value { get; set; }
public AdminSelect OnChange(AdminAction handler)
{
Handler = handler;
return this;
}
}
public sealed class AdminNumber : AdminWidget
{
public string Label { get; set; } = string.Empty;
public int Value { get; set; }
public int? Min { get; set; }
public int? Max { get; set; }
public AdminNumber OnChange(AdminAction handler)
{
Handler = handler;
return this;
}
}
public sealed class AdminTable : AdminWidget
{
public List<string> Columns { get; set; } = new();
public List<List<AdminTableCell>> Rows { get; set; } = new();
}
public sealed class AdminChips : AdminWidget
{
public List<AdminChip> Items { get; set; } = new();
public string? Placeholder { get; set; }
public string? AddLabel { get; set; }
public string? AddAction { get; set; }
public string? RemoveAction { get; set; }
[JsonIgnore]
public AdminAction? AddHandler { get; set; }
[JsonIgnore]
public AdminAction? RemoveHandler { get; set; }
public AdminChips OnAdd(AdminAction handler)
{
AddHandler = handler;
return this;
}
public AdminChips OnRemove(AdminAction handler)
{
RemoveHandler = handler;
return this;
}
}
public sealed class AdminDivider : AdminWidget
{
}
public sealed record AdminOption(string Label, string Value);
public sealed class AdminTableCell
{
public string Text { get; set; } = string.Empty;
public string? Tone { get; set; }
public bool Monospace { get; set; }
}
public sealed record AdminChip(string Value, string Label);
+22
View File
@@ -0,0 +1,22 @@
namespace Empostor.Api.Admin;
/// <summary>
/// Implemented by a plugin to contribute a panel to the admin interface
/// </summary>
public interface IAdminExtension
{
/// <summary>Unique, stable id. Also the last path segment of <c>/api/admin/ext/{Id}</c>.</summary>
string Id { get; }
/// <summary>Display title shown in the sidebar and panel header.</summary>
string Title { get; }
/// <summary>Icon name (from the built-in icon set), rendered as an inline glyph.</summary>
string Icon => "puzzle";
/// <summary>Sidebar grouping label. Defaults to "Plugins".</summary>
string Section => "Plugins";
/// <summary>Declare the panel's widgets and callbacks.</summary>
void Build(AdminPanelBuilder builder);
}
+6
View File
@@ -8,5 +8,11 @@ namespace Empostor.Api.Config
public string MarketplaceUrl { get; set; } =
"https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/plugins.json";
/// <summary>Active admin theme id. Themes live under <c>Pages/themes/{Id}</c> or as plugins.</summary>
public string Theme { get; set; } = "default";
/// <summary>Initial color mode ("light" or "dark"). Operators can still toggle it in the panel.</summary>
public string ThemeMode { get; set; } = "dark";
}
}
-10
View File
@@ -3,9 +3,7 @@ namespace Empostor.Api.Config;
public enum AuthApiMode
{
Innersloth,
Niko,
Both,
Relay,
Ume,
}
@@ -15,14 +13,6 @@ public class AuthApiConfig
public AuthApiMode Mode { get; set; } = AuthApiMode.Innersloth;
public string NikoApiKey { get; set; } = string.Empty;
public string NikoApiBaseUrl { get; set; } = "https://au-verify.niko233.top";
public string RelayApiBaseUrl { get; set; } = "http://localhost:5100";
public string RelayApiKey { get; set; } = string.Empty;
public string UmeApiBaseUrl { get; set; } = "https://auverify.hayashiume.top";
public string UmeApiKey { get; set; } = "sk-empostor-globalapikey";
@@ -1,19 +0,0 @@
using System.Collections.Generic;
namespace Empostor.Api.Config
{
public class ChatFilterConfig
{
public const string Section = "ChatFilter";
public bool Enabled { get; set; } = false;
public List<string> BlockedWords { get; set; } = new();
public bool BlockMessage { get; set; } = true;
public int SpamThreshold { get; set; } = 5;
public int SpamWindowSeconds { get; set; } = 10;
}
}
@@ -1,18 +0,0 @@
namespace Empostor.Api.Config
{
public class DiscordWebhookConfig
{
public const string Section = "DiscordWebhook";
public string MatchmakerUrl { get; set; } = string.Empty;
public string AdminUrl { get; set; } = string.Empty;
public bool? Enabled { get; set; }
public string? WebhookUrl { get; set; }
public bool? NotifyOnGameCreated { get; set; }
public bool? NotifyOnBan { get; set; }
public bool? NotifyOnReport { get; set; }
public bool? NotifyOnPlayerJoin { get; set; }
public bool? NotifyOnGameEnded { get; set; }
}
}
@@ -1,11 +0,0 @@
namespace Empostor.Api.Config
{
public class PlayerStatsConfig
{
public const string Section = "PlayerStats";
public bool Enabled { get; set; } = false;
public bool PersistToFile { get; set; } = true;
}
}
+5 -5
View File
@@ -6,17 +6,20 @@
<Nullable>enable</Nullable>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<VersionPrefix>2.0.0</VersionPrefix>
<AssemblyName>Empostor.Api</AssemblyName>
<AssemblyTitle>Empostor.Api</AssemblyTitle>
<Authors>AeonLucid</Authors>
<Description>An api library for Empostor, an Among Us private server. You need this package to write plugins for Empostor.</Description>
<PackageId>Empostor.Api</PackageId>
<PackageTags>Among Us;Empostor;Empostor Plugin</PackageTags>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<PackageIconUrl>https://raw.githubusercontent.com/Empostor/Empostor/dev/docs/images/logo_100.png</PackageIconUrl>
<PackageIcon>logo_100.png</PackageIcon>
<PackageProjectUrl>https://github.com/Empostor/Empostor</PackageProjectUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Empostor/Empostor</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -25,16 +28,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Empostor.Hazel" Version="1.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<ProjectReference Include="..\Empostor.Api.Innersloth.Generator\Empostor.Api.Innersloth.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<ProjectReference Include="..\NextFast.Hazel-New\Next.Hazel.Abstractions\Next.Hazel.Abstractions.csproj" />
<ProjectReference Include="..\NextFast.Hazel-New\Next.Hazel\Next.Hazel.csproj" />
<ProjectReference Include="..\Empostor.Api.Innersloth.Generator\Empostor.Api.Innersloth.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" PrivateAssets="all" />
<AdditionalFiles Include="Innersloth/Data/**/*.json" />
</ItemGroup>
@@ -1,4 +1,4 @@
using Empostor.Api.Net.Inner.Objects;
using Empostor.Api.Net.Inner.Objects;
namespace Empostor.Api.Events.Meeting
{
@@ -13,5 +13,15 @@ namespace Empostor.Api.Events.Meeting
/// Gets a value indicating whether meeting result is a tie.
/// </summary>
bool IsTie { get; }
/// <summary>
/// Gets a value indicating whether a Judge overruled the vote result.
/// </summary>
bool WasOverruled { get; }
/// <summary>
/// Gets the nonce of the winning Judge overrule, or 0 if the meeting wasn't overruled.
/// </summary>
ushort OverrideId { get; }
}
}
+6
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
@@ -49,6 +50,11 @@ namespace Empostor.Api.Games
/// </remarks>
bool IsHostAuthoritive { get; }
/// <summary>
/// Gets the mod GUID that the game was registered with through the AMCI.
/// </summary>
Guid? ModGuid { get; }
IClientPlayer? GetClientPlayer(int clientId);
T? FindObjectByNetId<T>(uint netId)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
{
"Invalid": 0,
"VisualTasks": 1,
"GhostsDoTasks": 2,
"ConfirmImpostor": 3,
"AnonymousVotes": 4,
"IsDefaults": 5,
"UseFlashlight": 6,
"SeekerFinalVents": 7,
"SeekerFinalMap": 8,
"SeekerPings": 9,
"ShowCrewmateNames": 10,
"Roles": 11,
"ShapeshifterLeaveSkin": 1000,
"ImpostorsCanSeeProtect": 1100,
"NoisemakerImpostorAlert": 1300
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"MapId": 1
}
@@ -0,0 +1,14 @@
{
"KillCooldown": 0,
"PlayerSpeed": 1,
"EmergencyMeetingCount": 2,
"EmergencyMeetingCooldown": 3,
"DiscussionTime": 4,
"VotingTime": 5,
"CrewmateVision": 6,
"HidingTime": 7,
"MaxTimeInVent": 8,
"ImposterVision": 9,
"FinalHideTime": 10,
"FinalHideImposterSpeed": 11
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"KillDistances": 1
}
@@ -0,0 +1,31 @@
{
"Invalid": 0,
"KillCooldown": 1,
"PlayerSpeedMod": 2,
"ImpostorLightMod": 3,
"CrewLightMod": 4,
"CrewmateTimeInVent": 100,
"FinalEscapeTime": 101,
"EscapeTime": 102,
"SeekerFinalSpeed": 103,
"MaxPingTime": 104,
"CrewmateFlashlightSize": 105,
"ImpostorFlashlightSize": 106,
"ShapeshifterCooldown": 1000,
"ShapeshifterDuration": 1001,
"ProtectionDurationSeconds": 1100,
"GuardianAngelCooldown": 1101,
"ScientistCooldown": 1200,
"ScientistBatteryCharge": 1201,
"EngineerCooldown": 1300,
"EngineerInVentMaxTime": 1301,
"PhantomCooldown": 1500,
"PhantomDuration": 1501,
"TrackerCooldown": 1550,
"TrackerDuration": 1551,
"TrackerDelay": 1552,
"NoisemakerAlertDuration": 1600,
"ViperDissolveTime": 1601,
"DetectiveSuspectLimit": 1602,
"JudgeTaskRequirementPercentage": 1620
}
@@ -0,0 +1,5 @@
{
"Invalid": 0,
"MaxImpostors": 1,
"MinPlayers": 2
}
@@ -0,0 +1,21 @@
{
"Invalid": 0,
"NumImpostors": 1,
"KillDistance": 2,
"NumEmergencyMeetings": 3,
"EmergencyCooldown": 4,
"DiscussionTime": 5,
"VotingTime": 6,
"MaxImpostors": 7,
"MinPlayers": 8,
"MaxPlayers": 9,
"NumCommonTasks": 10,
"NumShortTasks": 11,
"NumLongTasks": 12,
"TaskBarMode": 13,
"Tag": 14,
"CrewmatesRemainingForVitals": 100,
"CrewmateVentUses": 101,
"ImpostorPlayerID": 102,
"RulePreset": 200
}
@@ -11,5 +11,6 @@
"Phantom": 9,
"Tracker": 10,
"Detective": 12,
"Viper": 18
"Viper": 18,
"Judge": 19
}
@@ -59,5 +59,6 @@
"CheckVanish": 62,
"StartVanish": 63,
"CheckAppear": 64,
"StartAppear": 65
"StartAppear": 65,
"QueueOverruleVotes": 66
}
@@ -626,6 +626,14 @@
"SanctionSelfHarmPromotion": 1720,
"SanctionRepeatOffender10": 1721,
"SanctionUnknown": 1722,
"ReplicatorAbility": 1800,
"ReplicatorRole": 1801,
"ReplicatorBlurb": 1802,
"ReplicatorBlurbMed": 1803,
"ReplicatorBlurbLong": 1804,
"ReplicatorCooldown": 1805,
"ReplicatorDuration": 1806,
"ReplicatorQuantity": 1807,
"ScreenShakeOption": 1900,
"FeaturedItems": 1901,
"FeaturedBundles": 1902,
@@ -1576,6 +1584,14 @@
"XboxCommsCreateGameError": 3702,
"XboxCommsSearchError": 3703,
"XboxCommsJoinError": 3704,
"WatchAdBody": 3800,
"WatchAdTitle": 3801,
"WatchAd": 3802,
"RewardAdTitle": 3803,
"RewardAdBody1": 3804,
"RewardAdBody2": 3805,
"WatchAdErrorBody": 3806,
"WatchAdErrorTitle": 3807,
"DetectiveRole": 4000,
"DetectiveAbility": 4001,
"DetectiveAbilityNotes": 4002,
@@ -1623,5 +1639,28 @@
"DetectiveNotesPrepositionMaybe": 4079,
"DetectiveNotesTitlePostIt": 4080,
"DetectiveNotesPostIt": 4081,
"DetectiveNotesSuspectNumber": 4082
"DetectiveNotesSuspectNumber": 4082,
"AprilFoolsMode": 4083,
"MatchInfoGuideTitle": 4084,
"MatchInfoGuidePlayersLabel": 4085,
"MatchInfoGuideNoRolesMessage": 4086,
"JudgeRole": 4090,
"JudgeAbility": 4091,
"JudgeBlurb": 4092,
"JudgeBlurbMed": 4093,
"JudgeBlurbLong": 4094,
"JudgeTaskRequirement": 4095,
"RolesHelp_Judge_01": 4096,
"RolesHelp_Judge_02": 4097,
"JudgeAbilityUnlocked": 4098,
"JudgeAbilityProgress": 4099,
"JudgeCourtAdjourned": 4100,
"JudgeHasSpoken": 4101,
"StatsJudgeSuccessfulOverrules": 4102,
"StatsJudgeFailedOverrules": 4103,
"QCJudges": 4104,
"QCJudgeOverrule": 4105,
"QCJudgePlease": 4106,
"QCJudgeWillOverrule": 4107,
"JudgeBeatYouToIt": 4108
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"Keywords": 1
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.1-dev",
"gameVersion": "2025.10.14",
"gameVersion": "2026.8.18",
"platformType": "StandaloneItch"
}
@@ -0,0 +1,37 @@
{
"hatIds": [
"hat_pk04_beanie",
"hat_pk05_Cheese",
"hat_pk05_Cherry",
"hat_Chocolate",
"hat_Zipper",
"hat_pk02_Crown",
"hat_babybean",
"hat_Sorry",
"hat_mareLwyd",
"hat_fishingHat",
"hat_schnapp",
"hat_pk03_Traffic",
"hat_Pot",
"hat_AbominalHat",
"hat_tombstone",
"hat_pk04_MiniCrewmate",
"hat_pk05_Wizardhat",
"hat_pk05_Ellryhat",
"hat_pk05_EllieToppat"
],
"visorIds": [
"visor_pk01_AngeryVisor",
"visor_pk01_DumStickerVisor",
"visor_vr_Vr-Black",
"visor_pk01_PaperMaskVisor",
"visor_wash"
],
"petIds": [
"pet_YuleGoatPet",
"pet_HamPet",
"pet_Lava",
"pet_Snow",
"pet_Bush"
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class BoolGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "bool";
[JsonPropertyName("AcceptedValues")]
public required List<bool> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required BoolOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class CategorizedGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "cat";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required CategorizedOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ChatModeGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "chat";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,87 @@
using System;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilter
{
[JsonConstructor]
public GameFilter(string optionType, string key, string subFilterString)
{
this.OptionType = optionType;
this.Key = key;
this.SubFilterString = subFilterString;
this.SubFilter = ResolveSubFilter(OptionType, SubFilterString);
}
[JsonIgnore]
public ISubFilter SubFilter { get; set; }
[JsonPropertyName("OptionType")]
public required string OptionType { get; set; }
[JsonPropertyName("Key")]
public required string Key { get; set; }
[JsonPropertyName("SubFilterString")]
public required string SubFilterString { get; set; }
public void ModifySubFilter(ISubFilter subFilter)
{
this.SubFilter = subFilter;
}
[OnSerializing]
internal void OnSerializing(StreamingContext context)
{
this.SubFilterString = JsonSerializer.Serialize(this.SubFilter);
}
[OnDeserialized]
internal void OnDeserialized(StreamingContext context)
{
this.SubFilter = this.ResolveSubFilter(this.OptionType, this.SubFilterString);
}
private ISubFilter ResolveSubFilter(string type, string filterString)
{
if (type != null)
{
switch (type.ToLower())
{
case "int":
return JsonSerializer.Deserialize<IntGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for IntGameFilter");
case "platform":
return JsonSerializer.Deserialize<PlatformGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for PlatformGameFilter");
case "cat":
return JsonSerializer.Deserialize<CategorizedGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for CategorizedGameFilter");
case "languages":
return JsonSerializer.Deserialize<LanguageFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for LanguageFilter");
case "chat":
return JsonSerializer.Deserialize<ChatModeGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ChatModeGameFilter");
case "map":
return JsonSerializer.Deserialize<MapGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for MapGameFilter");
case "bool":
return JsonSerializer.Deserialize<BoolGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for BoolGameFilter");
case "mod":
return JsonSerializer.Deserialize<ModFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ModFilter");
default:
throw new InvalidOperationException("No type matches subfilter");
}
}
throw new InvalidOperationException("Must provide type for sub filter");
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilterSet
{
[JsonConstructor]
public GameFilterSet(GameModes gameMode, List<GameFilter> filters)
{
GameMode = gameMode;
Filters = filters;
}
[JsonPropertyName("GameMode")]
public required GameModes GameMode { get; set; }
[JsonPropertyName("Filters")]
public required List<GameFilter> Filters { get; set; }
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFiltersList
{
[JsonPropertyName("FilterSets")]
public required List<GameFilterSet> FilterSets { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Empostor.Api.Innersloth.GameFilters
{
public interface ISubFilter
{
string FilterType { get; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class IntGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "int";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required Int32OptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class LanguageFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "languages";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class MapGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "map";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,16 @@
using System;
using System.Text.Json.Serialization;
// https://github.com/Innersloth-LLC/AmongUsModdingInformation
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ModFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "mod";
[JsonPropertyName("AcceptedValues")]
public required Guid AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class PlatformGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "platform";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -2,7 +2,7 @@ namespace Empostor.Api.Innersloth.GameOptions;
public class HideNSeekGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 11;
public HideNSeekGameOptions(byte version = LatestVersion)
{
@@ -107,6 +107,11 @@ public class HideNSeekGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -142,11 +147,6 @@ public class HideNSeekGameOptions : IGameOptions
{
Tag = (GameTags)reader.ReadByte();
}
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -49,6 +49,8 @@ public interface IGameOptions
/// </summary>
public bool IsDefaults { get; set; }
public GameTags Tag { get; set; }
public void Serialize(IMessageWriter writer);
public static void EnsureVersionIsModular<TCaller>(byte version)
@@ -145,6 +145,8 @@ public class LegacyGameOptionsData : IGameOptions
/// </summary>
public bool IsDefaults { get; set; } = true;
public GameTags Tag { get; set; } = 0;
public static LegacyGameOptionsData Deserialize(IMessageReader reader, byte version)
{
var options = new LegacyGameOptionsData(version);
@@ -4,7 +4,7 @@ namespace Empostor.Api.Innersloth.GameOptions;
public class NormalGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 11;
public NormalGameOptions(byte version = LatestVersion)
{
@@ -144,6 +144,11 @@ public class NormalGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -184,11 +189,6 @@ public class NormalGameOptions : IGameOptions
}
RoleOptions.Deserialize(reader);
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -0,0 +1,29 @@
namespace Empostor.Api.Innersloth.GameOptions.RoleOptions;
public class JudgeRoleOptions : IRoleOptions
{
public JudgeRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Judge;
public float JudgeTaskRequirementPercentage { get; set; } = 50f;
public static JudgeRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new JudgeRoleOptions(version);
options.JudgeTaskRequirementPercentage = (int)reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write((byte)JudgeTaskRequirementPercentage);
}
}
@@ -34,6 +34,7 @@ public class RoleOptionsCollection
RoleTypes.Tracker => TrackerRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Detective => DetectiveRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Viper => ViperRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Judge => JudgeRoleOptions.Deserialize(roleOptionsReader, Version),
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
+90 -16
View File
@@ -166,7 +166,6 @@ public sealed class LanguageService
"command.note.set": "Note set: {0}",
"command.color.invalid": "Invalid color ID. Valid range: 0–17.",
"command.color.set": "Color changed to {0} ({1}).",
"command.title.too_long": "Title too long. Max 12 characters.",
"command.title.cleared": "Title removed.",
"command.title.set": "Title set: {0}",
@@ -205,7 +204,12 @@ public sealed class LanguageService
"command.note.description": "Set or clear a lobby note (host only).",
"command.note.usage": "note <text> | note clear",
"command.max.description": "Set max players for the lobby (host only).",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "You are banned from this server.",
"ban.notice.reason": "Reason: {0}",
"ban.notice.unban": "Unban time: {0}",
"ban.notice.unban_permanent": "Unban time: Permanent",
"ban.notice.contact": "If you have any questions, please contact the administrator."
}
""";
@@ -260,7 +264,12 @@ public sealed class LanguageService
"command.note.description": "设置或清除房间备注(仅房主)。",
"command.note.usage": "note <文字> | note clear",
"command.max.description": "设置房间最大人数(仅房主)。",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "您已被服务器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封时间:{0}",
"ban.notice.unban_permanent": "解封时间:永久",
"ban.notice.contact": "如有问题请联系管理员"
}
""";
@@ -315,7 +324,12 @@ public sealed class LanguageService
"command.note.description": "設置或清除房間備註(僅房主)。",
"command.note.usage": "note <文字> | note clear",
"command.max.description": "設置房間最大人數(僅房主)。",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "您已被伺服器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封時間:{0}",
"ban.notice.unban_permanent": "解封時間:永久",
"ban.notice.contact": "如有問題請聯絡管理員"
}
""";
@@ -356,7 +370,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "이 명령어는 로비에서만 사용할 수 있습니다.",
"command.players.header": "=== 플레이어 ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "내 핑: {0}ms"
"command.ping.result": "내 핑: {0}ms",
"ban.notice.header": "이 서버에서 차단되었습니다.",
"ban.notice.reason": "사유: {0}",
"ban.notice.unban": "차단 해제 시간: {0}",
"ban.notice.unban_permanent": "차단 해제 시간: 영구",
"ban.notice.contact": "문의 사항이 있으면 관리자에게 연락해 주세요."
}
""";
@@ -397,7 +416,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Эта команда может использоваться только в лобби.",
"command.players.header": "=== Игроки ({0}) ===",
"command.players.entry": "{0} | {1} | {2}мс",
"command.ping.result": "Ваш пинг: {0}мс"
"command.ping.result": "Ваш пинг: {0}мс",
"ban.notice.header": "Вы забанены на этом сервере.",
"ban.notice.reason": "Причина: {0}",
"ban.notice.unban": "Время разбана: {0}",
"ban.notice.unban_permanent": "Время разбана: навсегда",
"ban.notice.contact": "Если у вас есть вопросы, обратитесь к администратору."
}
""";
@@ -438,7 +462,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Dieser Befehl kann nur in der Lobby verwendet werden.",
"command.players.header": "=== Spieler ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Dein Ping: {0}ms"
"command.ping.result": "Dein Ping: {0}ms",
"ban.notice.header": "Du wurdest von diesem Server gebannt.",
"ban.notice.reason": "Grund: {0}",
"ban.notice.unban": "Entbannungszeit: {0}",
"ban.notice.unban_permanent": "Entbannungszeit: Dauerhaft",
"ban.notice.contact": "Bei Fragen wende dich an den Administrator."
}
""";
@@ -479,7 +508,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Cette commande ne peut être utilisée que dans le lobby.",
"command.players.header": "=== Joueurs ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Votre ping : {0}ms"
"command.ping.result": "Votre ping : {0}ms",
"ban.notice.header": "Vous êtes banni de ce serveur.",
"ban.notice.reason": "Raison : {0}",
"ban.notice.unban": "Débannissement : {0}",
"ban.notice.unban_permanent": "Débannissement : Permanent",
"ban.notice.contact": "En cas de question, contactez un administrateur."
}
""";
@@ -520,7 +554,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "このコマンドはロビーでのみ使用できます。",
"command.players.header": "=== プレイヤー ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "あなたのPing: {0}ms"
"command.ping.result": "あなたのPing: {0}ms",
"ban.notice.header": "このサーバーからBANされています。",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "BAN解除日時:{0}",
"ban.notice.unban_permanent": "BAN解除日時:永久",
"ban.notice.contact": "ご不明な点があれば管理者にお問い合わせください。"
}
""";
@@ -561,7 +600,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contacte o administrador."
}
""";
@@ -600,7 +644,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contate o administrador."
}
""";
@@ -639,7 +688,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando solo puede usarse en el lobby.",
"command.players.header": "=== Jugadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Tu ping: {0}ms"
"command.ping.result": "Tu ping: {0}ms",
"ban.notice.header": "Has sido baneado de este servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Fin del baneo: {0}",
"ban.notice.unban_permanent": "Fin del baneo: Permanente",
"ban.notice.contact": "Si tienes dudas, contacta con un administrador."
}
""";
@@ -678,7 +732,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Questo comando può essere usato solo nella lobby.",
"command.players.header": "=== Giocatori ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Il tuo ping: {0}ms"
"command.ping.result": "Il tuo ping: {0}ms",
"ban.notice.header": "Sei stato bannato da questo server.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Sbanno: {0}",
"ban.notice.unban_permanent": "Sbanno: Permanente",
"ban.notice.contact": "Per qualsiasi domanda, contatta l'amministratore."
}
""";
@@ -717,7 +776,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Dit commando kan alleen in de lobby worden gebruikt.",
"command.players.header": "=== Spelers ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Jouw ping: {0}ms"
"command.ping.result": "Jouw ping: {0}ms",
"ban.notice.header": "Je bent geband van deze server.",
"ban.notice.reason": "Reden: {0}",
"ban.notice.unban": "Ontbantijd: {0}",
"ban.notice.unban_permanent": "Ontbantijd: Permanent",
"ban.notice.contact": "Neem bij vragen contact op met de beheerder."
}
""";
@@ -756,7 +820,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Ang command na ito ay maaari lamang gamitin sa lobby.",
"command.players.header": "=== Mga Manlalaro ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Ang iyong ping: {0}ms"
"command.ping.result": "Ang iyong ping: {0}ms",
"ban.notice.header": "Banned ka sa server na ito.",
"ban.notice.reason": "Dahilan: {0}",
"ban.notice.unban": "Oras ng pag-unban: {0}",
"ban.notice.unban_permanent": "Oras ng pag-unban: Permanente",
"ban.notice.contact": "Kung may tanong, makipag-ugnayan sa administrator."
}
""";
@@ -795,7 +864,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Ní féidir an t-ordú seo a úsáid ach sa stocaireacht.",
"command.players.header": "=== Imreoirí ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Do phing: {0}ms"
"command.ping.result": "Do phing: {0}ms",
"ban.notice.header": "Tá cosc ort ar an bhfreastalaí seo.",
"ban.notice.reason": "Cúis: {0}",
"ban.notice.unban": "Am dí-chosc: {0}",
"ban.notice.unban_permanent": "Am dí-chosc: Buan",
"ban.notice.contact": "Má tá ceist agat, déan teagmháil le riarthóir."
}
""";
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Empostor.Api.Events.Player;
@@ -6,6 +6,24 @@ namespace Empostor.Api.Net.Inner.Objects
{
public interface IInnerMeetingHud : IInnerNetObject
{
public interface IJudgeOverrule
{
/// <summary>
/// Gets the player id of the Judge that queued this overrule.
/// </summary>
byte JudgePlayerId { get; }
/// <summary>
/// Gets the player id of the player the Judge wants to overrule.
/// </summary>
byte OverruledPlayerId { get; }
/// <summary>
/// Gets the overrule nonce used to match this overrule to the voting result.
/// </summary>
ushort OverruleNonce { get; }
}
public interface IPlayerVoteArea
{
/// <summary>
@@ -55,5 +73,10 @@ namespace Empostor.Api.Net.Inner.Objects
/// Gets the player that started the meeting.
/// </summary>
IInnerPlayerInfo? Reporter { get; }
/// <summary>
/// Gets the Judge overrules queued in this meeting.
/// </summary>
IReadOnlyCollection<IJudgeOverrule> JudgeOverrules { get; }
}
}
@@ -0,0 +1,25 @@
using System;
using Empostor.Api.Innersloth;
using Empostor.Api.Innersloth.GameOptions;
namespace Empostor.Api.Net.Messages.C2S
{
/// <summary>
/// Special serialization rules for AU MCI HostGame messages.
/// <see href="https://github.com/Innersloth-LLC/AmongUsModdingInformation?tab=readme-ov-file#all-client-mod-client-identification"/>
/// for more information.
/// </summary>
public static class Message25HostModdedGameC2S
{
public static void Serialize(IMessageWriter writer, IGameOptions gameOptions, CrossplayFlags crossplayFlags, GameFilterOptions gameFilterOptions)
{
Message00HostGameC2S.Serialize(writer, gameOptions, crossplayFlags, gameFilterOptions);
}
public static void Deserialize(IMessageReader reader, out IGameOptions gameOptions, out CrossplayFlags crossplayFlags, out GameFilterOptions gameFilterOptions, out Guid modGuid)
{
Message00HostGameC2S.Deserialize(reader, out gameOptions, out crossplayFlags, out gameFilterOptions);
modGuid = new Guid(reader.ReadBytes(16).Span);
}
}
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -29,6 +29,7 @@ namespace Empostor.Api.Net.Messages
public const byte QueryPlatformIds = 22;
public const byte QueryLobbyInfo = 23;
public const byte EndGameHostMigration = 24;
public const byte HostModdedGame = 25;
public const byte PackedGameDataTo = 26;
private static readonly Dictionary<byte, string> FlagCache;
@@ -2,6 +2,8 @@ namespace Empostor.Api.Net.Messages.Rpcs
{
public static class Rpc23VotingComplete
{
// Pre 18.0 version
// TODO clean up when 17.4 and below are unsupported
public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie)
{
writer.WriteBytesAndSize(states);
@@ -9,6 +11,15 @@ namespace Empostor.Api.Net.Messages.Rpcs
writer.Write(tie);
}
public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie, bool wasOverruled, ushort overrideId)
{
writer.WriteBytesAndSize(states);
writer.Write(playerId);
writer.Write(tie);
writer.Write(wasOverruled);
writer.Write(overrideId);
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie)
{
var length = reader.ReadPackedInt32();
@@ -21,5 +32,21 @@ namespace Empostor.Api.Net.Messages.Rpcs
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie, out bool wasOverruled, out ushort overrideId)
{
var length = reader.ReadPackedInt32();
states = new IMessageReader[length];
for (var i = 0; i < length; i++)
{
states[i] = reader.ReadMessage();
}
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
wasOverruled = reader.ReadBoolean();
overrideId = reader.ReadUInt16();
}
}
}
@@ -0,0 +1,19 @@
namespace Empostor.Api.Net.Messages.Rpcs
{
public static class Rpc66QueueOverruleVotes
{
public static void Serialize(IMessageWriter writer, byte judgePlayerId, byte targetPlayerId, ushort overruleNonce)
{
writer.Write(judgePlayerId);
writer.Write(targetPlayerId);
writer.Write(overruleNonce);
}
public static void Deserialize(IMessageReader reader, out byte judgePlayerId, out byte targetPlayerId, out ushort overruleNonce)
{
judgePlayerId = reader.ReadByte();
targetPlayerId = reader.ReadByte();
overruleNonce = reader.ReadUInt16();
}
}
}
+2
View File
@@ -0,0 +1,2 @@
## Empostor.Api
An api library for Empostor, an Among Us private server. You need this package to write plugins for Empostor
+20 -15
View File
@@ -2,6 +2,19 @@
"version": 1,
"dependencies": {
"net8.0": {
"Empostor.Hazel": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==",
"dependencies": {
"BouncyCastle.Cryptography": "2.6.1",
"Empostor.Hazel.Abstractions": "1.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6",
"Microsoft.Extensions.ObjectPool": "8.0.1",
"Serilog": "4.3.1"
}
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Direct",
"requested": "[2.2.0, )",
@@ -48,6 +61,11 @@
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Empostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig=="
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -108,8 +126,8 @@
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
"resolved": "4.3.1",
"contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
},
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
@@ -125,19 +143,6 @@
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
+21 -18
View File
@@ -7,6 +7,23 @@
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Empostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==",
"dependencies": {
"BouncyCastle.Cryptography": "2.6.1",
"Empostor.Hazel.Abstractions": "1.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6",
"Microsoft.Extensions.ObjectPool": "8.0.1",
"Serilog": "4.3.1"
}
},
"Empostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -96,8 +113,8 @@
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
"resolved": "4.3.1",
"contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
@@ -112,25 +129,11 @@
"empostor.api": {
"type": "Project",
"dependencies": {
"Empostor.Hazel": "[1.0.2, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
@@ -0,0 +1,30 @@
using Empostor.Api.Admin;
namespace Empostor.Plugin.Code;
public sealed class CodeAdminExtension : IAdminExtension
{
private readonly IGameCodeManager _manager;
public CodeAdminExtension(IGameCodeManager manager)
{
_manager = manager;
}
public string Id => "game-codes";
public string Title => "Game Codes";
public string Icon => "database";
public void Build(AdminPanelBuilder b)
{
b.RegisterText(
$"Loaded {_manager.SixCharCodes} six-character and {_manager.FourCharCodes} four-character code(s).",
"default");
b.RegisterText($"Source directory: {_manager.Path}", "muted", monospace: true);
b.RegisterText(
"Codes are read from .txt files at startup. Add or edit files in the directory, then restart the server to reload.",
"muted");
}
}
+2
View File
@@ -1,3 +1,4 @@
using Empostor.Api.Admin;
using Empostor.Api.Events;
using Empostor.Api.Plugins;
using Empostor.Plugin.Code.Handlers;
@@ -14,5 +15,6 @@ public sealed class CodeStartup : IPluginStartup
{
services.AddSingleton<IGameCodeManager, GameCodeManager>();
services.AddSingleton<IEventListener, GameEventListener>();
services.AddSingleton<IAdminExtension, CodeAdminExtension>();
}
}
@@ -7,4 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
</ItemGroup>
<Target Name="CopyToMarketplace" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="E:\WindowsFile\Empostor\Empostor\marketplace" />
</Target>
</Project>
+21 -18
View File
@@ -7,6 +7,23 @@
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Empostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==",
"dependencies": {
"BouncyCastle.Cryptography": "2.6.1",
"Empostor.Hazel.Abstractions": "1.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6",
"Microsoft.Extensions.ObjectPool": "8.0.1",
"Serilog": "4.3.1"
}
},
"Empostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -96,8 +113,8 @@
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
"resolved": "4.3.1",
"contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
@@ -112,25 +129,11 @@
"empostor.api": {
"type": "Project",
"dependencies": {
"Empostor.Hazel": "[1.0.2, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
@@ -7,4 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
</ItemGroup>
<Target Name="CopyToMarketplace" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="E:\WindowsFile\Empostor\Empostor\marketplace" />
</Target>
</Project>
@@ -0,0 +1,97 @@
using Empostor.Api.Admin;
using Empostor.Api.Plugins;
namespace Empostor.Plugin.Narrator;
public sealed class NarratorAdminExtension : IAdminExtension
{
private readonly NarratorConfig _config;
private readonly string _configPath;
public NarratorAdminExtension(NarratorConfig config, string configPath)
{
_config = config;
_configPath = configPath;
}
public string Id => "narrator";
public string Title => "Narrator";
public string Icon => "chat";
public void Build(AdminPanelBuilder b)
{
b.RegisterTextbox(t =>
{
t.Label = "DeepSeek API key";
t.Value = _config.ApiKey;
t.Secret = true;
t.Placeholder = "sk-...";
t.OnSubmit(async ctx =>
{
_config.ApiKey = ctx.Value ?? string.Empty;
PluginConfigLoader.Save(_configPath, _config);
return AdminActionResult.Ok("API key saved.");
});
});
b.RegisterTextbox(t =>
{
t.Label = "Model";
t.Value = _config.Model;
t.OnSubmit(async ctx =>
{
_config.Model = ctx.Value ?? string.Empty;
PluginConfigLoader.Save(_configPath, _config);
return AdminActionResult.Ok("Model saved.");
});
});
b.RegisterTextbox(t =>
{
t.Label = "API endpoint";
t.Value = _config.ApiEndpoint;
t.OnSubmit(async ctx =>
{
_config.ApiEndpoint = ctx.Value ?? string.Empty;
PluginConfigLoader.Save(_configPath, _config);
return AdminActionResult.Ok("API endpoint saved.");
});
});
b.RegisterNumber(n =>
{
n.Label = "Max uses per game";
n.Value = _config.MaxUsesPerGame;
n.Min = 0;
n.OnChange(async ctx =>
{
if (int.TryParse(ctx.Value, out var v) && v >= 0)
{
_config.MaxUsesPerGame = v;
PluginConfigLoader.Save(_configPath, _config);
}
return AdminActionResult.Ok("Max uses per game saved (applies to new games).");
});
});
b.RegisterNumber(n =>
{
n.Label = "Max uses per meeting";
n.Value = _config.MaxUsesPerMeeting;
n.Min = 0;
n.OnChange(async ctx =>
{
if (int.TryParse(ctx.Value, out var v) && v >= 0)
{
_config.MaxUsesPerMeeting = v;
PluginConfigLoader.Save(_configPath, _config);
}
return AdminActionResult.Ok("Max uses per meeting saved (applies to new games).");
});
});
}
}
@@ -1,3 +1,4 @@
using Empostor.Api.Admin;
using Empostor.Api.Commands;
using Empostor.Api.Events;
using Empostor.Api.Plugins;
@@ -16,7 +17,8 @@ public sealed class NarratorStartup : IPluginStartup
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(_ => PluginConfigLoader.Load<NarratorConfig>(_configPath));
var config = PluginConfigLoader.Load<NarratorConfig>(_configPath);
services.AddSingleton(config);
services.AddSingleton<NarratorService>();
services.AddSingleton<NarratorCommand>();
services.AddSingleton<ICommand, NarratorCommand>(
@@ -24,5 +26,7 @@ public sealed class NarratorStartup : IPluginStartup
services.AddSingleton<NarratorEventListener>();
services.AddSingleton<IEventListener, NarratorEventListener>(
sp => sp.GetRequiredService<NarratorEventListener>());
services.AddSingleton<IAdminExtension, NarratorAdminExtension>(
_ => new NarratorAdminExtension(config, _configPath));
}
}
+21 -18
View File
@@ -7,6 +7,23 @@
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Empostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==",
"dependencies": {
"BouncyCastle.Cryptography": "2.6.1",
"Empostor.Hazel.Abstractions": "1.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6",
"Microsoft.Extensions.ObjectPool": "8.0.1",
"Serilog": "4.3.1"
}
},
"Empostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -96,8 +113,8 @@
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
"resolved": "4.3.1",
"contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
@@ -112,25 +129,11 @@
"empostor.api": {
"type": "Project",
"dependencies": {
"Empostor.Hazel": "[1.0.2, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
@@ -0,0 +1,98 @@
using System.Linq;
using Empostor.Api.Admin;
namespace Empostor.Plugins.ChatFilter;
public sealed class ChatFilterAdminExtension : IAdminExtension
{
private readonly ChatFilterStore _store;
public ChatFilterAdminExtension(ChatFilterStore store)
{
_store = store;
}
public string Id => "chat-filter";
public string Title => "Chat Filter";
public string Icon => "filter";
public void Build(AdminPanelBuilder b)
{
b.RegisterToggle(t =>
{
t.Label = "Enable filtering";
t.Value = _store.Enabled;
t.OnChange(async ctx =>
{
_store.Enabled = ctx.Value == "true";
await _store.SaveAsync();
return AdminActionResult.Ok("Chat filter " + (_store.Enabled ? "enabled." : "disabled."));
});
});
b.RegisterToggle(t =>
{
t.Label = "Block messages (off = log only)";
t.Value = _store.BlockMessage;
t.OnChange(async ctx =>
{
_store.BlockMessage = ctx.Value == "true";
await _store.SaveAsync();
return AdminActionResult.Ok("Saved.");
});
});
b.RegisterNumber(n =>
{
n.Label = "Spam threshold (messages)";
n.Value = _store.SpamThreshold;
n.Min = 0;
n.OnChange(async ctx =>
{
if (int.TryParse(ctx.Value, out var v))
{
_store.SpamThreshold = v;
await _store.SaveAsync();
}
return AdminActionResult.Ok("Spam threshold saved.");
});
});
b.RegisterNumber(n =>
{
n.Label = "Spam window (seconds)";
n.Value = _store.SpamWindowSeconds;
n.Min = 1;
n.OnChange(async ctx =>
{
if (int.TryParse(ctx.Value, out var v) && v > 0)
{
_store.SpamWindowSeconds = v;
await _store.SaveAsync();
}
return AdminActionResult.Ok("Spam window saved.");
});
});
b.RegisterChips(c =>
{
c.Placeholder = "Add a blocked word…";
c.AddLabel = "Add";
c.Items = _store.BlockedWords.Select(w => new AdminChip(w, w)).ToList();
c.OnAdd(async ctx =>
{
_store.AddWord(ctx.Value ?? string.Empty);
return AdminActionResult.Ok("Word added.");
});
c.OnRemove(async ctx =>
{
_store.RemoveWord(ctx.Value ?? string.Empty);
return AdminActionResult.Ok("Word removed.");
});
});
}
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Empostor.Plugins.ChatFilter;
public sealed class ChatFilterConfig
{
public bool Enabled { get; set; }
public List<string> BlockedWords { get; set; } = new();
public bool BlockMessage { get; set; } = true;
public int SpamThreshold { get; set; } = 5;
public int SpamWindowSeconds { get; set; } = 10;
}
@@ -5,9 +5,9 @@ using Empostor.Api.Events;
using Empostor.Api.Events.Player;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Chat;
namespace Empostor.Plugins.ChatFilter;
internal sealed class ChatFilterListener : IEventListener
public sealed class ChatFilterListener : IEventListener
{
private readonly ILogger<ChatFilterListener> _logger;
private readonly ChatFilterStore _store;
@@ -30,7 +30,6 @@ internal sealed class ChatFilterListener : IEventListener
var playerKey = e.ClientPlayer.Client.FriendCode ?? e.ClientPlayer.Client.Name;
var message = e.Message;
// Word filter
var words = _store.BlockedWords;
if (words.Count > 0)
{
@@ -63,7 +62,6 @@ internal sealed class ChatFilterListener : IEventListener
}
}
// Spam rate limit
var threshold = _store.SpamThreshold;
if (threshold > 0)
{
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Empostor.Api.Plugins;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.ChatFilter;
[EmpostorPlugin("cn.hayashiume.chatfilter", "Chat Filter", "HayashiUme", "2.0.0")]
public sealed class ChatFilterPlugin : PluginBase
{
private readonly ILogger<ChatFilterPlugin> _logger;
public ChatFilterPlugin(ILogger<ChatFilterPlugin> logger)
{
_logger = logger;
}
public override ValueTask EnableAsync()
{
_logger.LogInformation("[ChatFilter] Enabled. Configure rules in the admin panel (Plugins → Chat Filter).");
return default;
}
public override ValueTask DisableAsync() => default;
}
@@ -0,0 +1,19 @@
using Empostor.Api.Admin;
using Empostor.Api.Events;
using Empostor.Api.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Empostor.Plugins.ChatFilter;
public sealed class ChatFilterPluginStartup : IPluginStartup
{
public void ConfigureHost(IHostBuilder host) { }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ChatFilterStore>();
services.AddSingleton<IEventListener, ChatFilterListener>();
services.AddSingleton<IAdminExtension, ChatFilterAdminExtension>();
}
}
@@ -1,28 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Empostor.Api.Config;
using Empostor.Api.Plugins;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Empostor.Server.Service.Admin.Chat;
namespace Empostor.Plugins.ChatFilter;
public sealed class ChatFilterStore : JsonDataStore<ChatFilterConfig>
{
private readonly object _wordsLock = new();
private const string ConfigFile = "[Empostor.Plugins.ChatFilter]Config.json";
private readonly object _wordsLock = new();
private List<string> _blockedWords = new();
public ChatFilterStore(ILogger<ChatFilterStore> logger, IOptions<ChatFilterConfig> config)
public ChatFilterStore(ILogger<ChatFilterStore> logger)
: base(logger, legacyPath: null)
{
Load();
// If no persisted file was loaded, fall back to config.json defaults
if (_blockedWords.Count == 0 && !Enabled)
{
var cfg = config.Value;
var cfg = PluginConfigLoader.Load<ChatFilterConfig>(ConfigPath());
Enabled = cfg.Enabled;
BlockMessage = cfg.BlockMessage;
SpamThreshold = cfg.SpamThreshold;
@@ -121,4 +121,6 @@ public sealed class ChatFilterStore : JsonDataStore<ChatFilterConfig>
_blockedWords = new List<string>(data.BlockedWords ?? new List<string>());
}
}
private static string ConfigPath() => Path.Combine(Directory.GetCurrentDirectory(), ConfigFile);
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Empostor.Plugins.ChatFilter</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,140 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Empostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==",
"dependencies": {
"BouncyCastle.Cryptography": "2.6.1",
"Empostor.Hazel.Abstractions": "1.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6",
"Microsoft.Extensions.ObjectPool": "8.0.1",
"Serilog": "4.3.1"
}
},
"Empostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.6",
"contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA=="
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Diagnostics.DiagnosticSource": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1",
"contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"empostor.api": {
"type": "Project",
"dependencies": {
"Empostor.Hazel": "[1.0.2, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
}
}
}
}
}
@@ -0,0 +1,51 @@
using Empostor.Api.Admin;
namespace Empostor.Plugins.DiscordWebhook;
public sealed class DiscordWebhookAdminExtension : IAdminExtension
{
private readonly DiscordWebhookStore _store;
public DiscordWebhookAdminExtension(DiscordWebhookStore store)
{
_store = store;
}
public string Id => "discord-webhook";
public string Title => "Discord Webhook";
public string Icon => "webhook";
public void Build(AdminPanelBuilder b)
{
b.RegisterBlock(block =>
{
block.Title = "Webhook URLs";
block.Description = "Leave a URL empty to disable its notifications.";
block.Children.Add(new AdminTextbox
{
Label = "Matchmaker URL",
Value = _store.MatchmakerUrl,
Placeholder = "https://discord.com/api/webhooks/...",
}.OnSubmit(async ctx =>
{
_store.MatchmakerUrl = ctx.Value ?? string.Empty;
await _store.SaveAsync();
return AdminActionResult.Ok("Matchmaker webhook saved.");
}));
block.Children.Add(new AdminTextbox
{
Label = "Admin URL",
Value = _store.AdminUrl,
Placeholder = "https://discord.com/api/webhooks/...",
}.OnSubmit(async ctx =>
{
_store.AdminUrl = ctx.Value ?? string.Empty;
await _store.SaveAsync();
return AdminActionResult.Ok("Admin webhook saved.");
}));
});
}
}
@@ -0,0 +1,11 @@
namespace Empostor.Plugins.DiscordWebhook;
public sealed class DiscordWebhookConfig
{
public string MatchmakerUrl { get; set; } = string.Empty;
public string AdminUrl { get; set; } = string.Empty;
/// <summary>Legacy single-webhook fallback used when both URLs are empty.</summary>
public string WebhookUrl { get; set; } = string.Empty;
}
@@ -7,11 +7,12 @@ using System.Text.Json;
using System.Threading.Tasks;
using Empostor.Api.Events;
using Empostor.Api.Events.Player;
using Empostor.Api.Innersloth;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Api;
namespace Empostor.Plugins.DiscordWebhook;
internal sealed class DiscordWebhookListener : IEventListener
public sealed class DiscordWebhookListener : IEventListener
{
private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = false };
@@ -29,8 +30,6 @@ internal sealed class DiscordWebhookListener : IEventListener
_config = config;
}
// ── Matchmaker events (fire-and-forget, do not block game flow) ──
[EventListener]
public void OnGameCreated(IGameCreatedEvent e)
{
@@ -90,8 +89,6 @@ internal sealed class DiscordWebhookListener : IEventListener
});
}
// ── Admin events (fire-and-forget) ──
[EventListener]
public void OnPlayerLeft(IGamePlayerLeftEvent e)
{
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Empostor.Api.Plugins;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.DiscordWebhook;
[EmpostorPlugin("cn.hayashiume.discordwebhook", "Discord Webhook", "HayashiUme", "2.0.0")]
public sealed class DiscordWebhookPlugin : PluginBase
{
private readonly ILogger<DiscordWebhookPlugin> _logger;
public DiscordWebhookPlugin(ILogger<DiscordWebhookPlugin> logger)
{
_logger = logger;
}
public override ValueTask EnableAsync()
{
_logger.LogInformation("[DiscordWebhook] Enabled. Configure URLs in the admin panel (Plugins → Discord Webhook).");
return default;
}
public override ValueTask DisableAsync() => default;
}
Loaded 100 of 299 files, more files were not shown because too many files have changed in this diff. Show more