Everyone who joins goes into a queue and gets paired into an arena. Win your duel and you move up, lose it and you move down — which arena you are in is your rank, so there is no ranking plugin to run and no database to host. Each pairing rolls a round type both players have enabled, and weapon preferences persist between sessions.
Arenas can be placed by hand on any map with an in-game editor, so you are not limited to maps built with paired arena spawns — and ready-made arenas ship for all nine active-duty maps, so it plays out of the box on dust2, mirage, inferno, nuke, ancient, anubis, overpass, train and vertigo.
Copy into your ModSharp install (<sharp> = your sharp directory):
| From | To |
|---|---|
.build/modules/Duels.Core/ |
<sharp>/modules/Duels.Core/ |
.build/shared/Duels.Shared/ |
<sharp>/shared/Duels.Shared/ |
.assets/locales/duels.json |
<sharp>/locales/duels.json |
.assets/configs/duels/maps/ |
<sharp>/configs/duels/maps/ |
Restart the server (or change map) to load. configs/duels/duels.json is written with defaults on first
run, and duels.cfg installs itself into csgo/cfg/duels/ and is exec'd on every map load.
⚠️ Never place a second copy ofDuels.Shared.dllin the module folder — it must resolve fromshared/, or its types get a separate assembly-load-context identity and API lookups silently return null.
Uses the ModSharp first-party modules (they ship with ModSharp): CommandCenter (every command), MenuManager (the weapon/round preference menus), ClientPreferences (persisting those preferences), LocalizerManager (all player-facing text), AdminManager (gating the two admin commands), TargetingManager (resolving a player by partial name for challenges).
Every one is resolved optionally — the plugin loads and runs without any of them, losing only the feature each powers.
| Command | Aliases | Description | Permission |
|---|---|---|---|
!queue |
— | Your current position in the waiting queue. | |
!guns |
gunpref, weaponpref |
Weapon preference menu, per category. | |
!rounds |
roundpref |
Enable/disable round types for yourself (at least one must stay on). | |
!afk |
— | Toggle AFK — leaves the queue and moves you to spectate. | |
!challenge <name> |
duel |
Challenge a specific player to a direct duel. | |
!caccept |
capprove |
Accept a pending challenge. | |
!cdecline |
cdeny |
Decline a pending challenge. | |
!duels |
— | List every arena with its round type and occupants. | @duels/admin |
!arenaedit <sub> |
— | Place arenas by hand — see below. | @duels/admin |
Command names and aliases are configurable.
Stand where a player should spawn, run add twice — once per side — then save.
| Subcommand | Effect |
|---|---|
add |
Store your current position and facing as a spawn. Two calls make one arena. |
cancel |
Discard a half-placed arena. |
list |
List placed arenas with coordinates. |
del <n> |
Remove arena n. |
clear |
Empty the working set. |
save |
Write to disk and hot-reload the live arena list. |
revert |
Discard unsaved edits and re-read the file. |
tp <n> |
Teleport to arena n to check placement. |
Edits stay in memory until save, so a misplaced spawn costs nothing.
configs/duels/duels.json (auto-generated on first run):
| Setting | Default | Meaning |
|---|---|---|
round-timer-seconds |
30 |
Hard cap on a live round, timed from freeze-end. 0 disables it. |
round-settings |
[] |
Empty uses the ten built-in round types; non-empty replaces them entirely. |
force-arena-clantags |
true |
Show each player's arena in their clan tag. |
disable-clantags |
false |
Suppress clan tag handling altogether. |
block-damage-of-not-opponent |
false |
Block damage between players in different arenas. |
block-flash-of-not-opponent |
false |
Block flash between players in different arenas. |
block-sound-of-not-opponent |
true |
Strip players in other arenas from a player's sound recipients. |
arena-bounds.enabled |
true |
Keep players inside their own arena. |
arena-bounds.radius-scale |
1.6 |
Multiplier on half the spawn separation. Higher = roomier. |
arena-bounds.padding |
220 |
Flat units added on top, so tight arenas aren't claustrophobic. |
arena-visibility.enabled |
true |
Hide players in other arenas, per-viewer. |
arena-visibility.hide-weapons |
true |
Hide their weapons too (else guns float in mid-air). |
arena-visibility.transmit-channel |
3 |
Dedicated channel so it composes with other transmit plugins. |
arena-visibility.refresh-seconds |
0.5 |
Full re-push interval; also reconciles mid-round weapon changes. |
give-knife-by-default |
true |
Always include a knife. |
prevent-draw-rounds |
true |
Resolve draws rather than leaving a round unwon. |
scoreboard-arena-grouping |
true |
Group the scoreboard by arena rank. |
pistol-armor-balance |
true |
Helmet off, kevlar only for cheap pistols, on pistol rounds. |
allowed-weapon-prefs |
all true |
Per-category toggles: rifle, sniper, smg, lmg, shotgun, pistol. |
default-weapon-settings |
— | Fallback weapon per category, plus default-round. |
Round types — ten built-ins, all 1v1: Rifle · Sniper · Shotgun · Pistol · Scout · AWP · Deagle · SMG · LMG · Knife. Rifle/Sniper/Shotgun/SMG/LMG use each player's preferred weapon in that category; Scout, AWP and Deagle force a specific one. A pairing only rolls a type both players have enabled.
Custom arenas — configs/duels/maps/<mapname>.json, written by arenaedit save and read on every
map load. When a file exists it replaces spawn-clustering for that map.
24 arenas per map ship for the nine active-duty maps, derived from
OpenPrefirePrac's practice profiles (MIT — see
THIRD-PARTY-NOTICES.md) by tools/prefire_to_arenas.py. Its bot positions are
placed to be shot at from the player position, which implies line of sight, so pairing them gives duels
across real competitive angles. They are a starting set: trim what you don't like with arenaedit list / tp <n> / del <n>, then save.
[
{
"Id": 0,
"Spawn1": { "posX": -7994.4, "posY": -3898.1, "posZ": -192.3, "angleX": 8.9, "angleY": -41.9, "angleZ": 0 },
"Spawn2": { "posX": -7261.5, "posY": -4598.2, "posZ": -395.9, "angleX": -15.1, "angleY": 133.2, "angleZ": 0 }
}
]Players sit in a queue and are drained into arenas at round start, one duel per arena. When a duel ends
the winner moves toward arena 1 and the loser moves the other way, so standing is positional rather than
a stored score. Round ends are driven by the plugin rather than CS round-win conditions, which is why
the shipped duels.cfg neutralises team money, score and round limits.
Arena geometry comes from a hand-placed spawn file when one exists, otherwise from clustering the map's
info_player_terrorist/counterterrorist entities into pairs. Hand-placed spawns also carry a facing
angle, so both players start looking at each other.
Players are kept in their own arena by a boundary derived from its two spawns — a cylinder centred between them — enforced with a velocity nudge rather than a teleport. Nothing to draw or maintain: every arena is bounded automatically. Sounds from other arenas are stripped per-recipient, because with two dozen arenas on one map, hearing every other duel makes sound cues worthless.
Players in other arenas are hidden per-viewer. The pawn itself cannot be transmit-hooked, so the player's controller is hooked instead — the engine then clears the pawn's transmit bit for the denied receiver while leaving the controller intact, so scoreboard and team data still work. Because the denial is per-receiver, you stay fully visible to your own opponent and disappear for everyone else. Held weapons are hidden with their owner, otherwise guns float where an invisible player stands.
Other plugins consume IDuelsShared (resolve in OnAllModulesLoaded) for arena placement lookups, AFK
state, and RegisterRoundType to add their own round behaviour:
var api = sharpModuleManager
.GetOptionalSharpModuleInterface<IDuelsShared>(IDuelsShared.Identity)?.Instance;Reference Duels.Shared with ExcludeAssets="runtime" and never ship your own copy of the DLL.
dotnet build -c ReleaseOutputs .build/modules/Duels.Core/Duels.dll and .build/shared/Duels.Shared/Duels.Shared.dll.
Derived from yappershq/Arenas, our ModSharp port of K4ryuu/K4-Arenas (GPL-3.0) — the original arena gamemode and the source of the ladder mechanic. Licensed under the same terms as that upstream, see LICENSE.
Feature set informed by Letaryat/CS2_Poor_Duels and zhw1nq/zDuels (CounterStrikeSharp). No code is taken from either — neither declares a license — but the custom-spawn file format is deliberately compatible, so spawn files in that shape load unmodified. Detail in docs/PROVENANCE.md.
Made with ❤️ by yappershq
⭐ Star this repo if you find it useful!