@amxts/resemiclip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kukson777
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.svg" width="96" alt="ReSemiclip">
4
+
5
+ # ReSemiclip
6
+
7
+ *Who walks through whom, as a rule over two players*
8
+
9
+ [![amxts module](https://img.shields.io/badge/amxts-module-3178c6?style=flat-square)](https://amxts.github.io/)
10
+ [![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
11
+
12
+ [Features](#features) • [Installation](#installation) • [Usage](#usage) • [API](#api)
13
+
14
+ **English** | [Русский](README.ru.md)
15
+
16
+ </div>
17
+
18
+ Semiclip lets players walk through each other. The [ReSemiclip](https://github.com/Next21Team/resemiclip-amxx) module does it on the server; this module lets an amxts plugin say who walks through whom as a rule over two players, `(player, target) => boolean`, and keeps ReSemiclip up to date with it.
19
+
20
+ ## Features
21
+
22
+ - **A rule, not bit masks.** One function says whether one player walks through another; the module works every pair out and tells ReSemiclip.
23
+ - **Kept up to date by itself.** A spawn, a death, a team change, a player coming or leaving, and a change of any field plugins add to `Player` work the pairs out again - only the pairs of the player it happened to.
24
+ - **Missing module said once.** Without ReSemiclip on the server nothing is set, and the console says why, once.
25
+ - **One instance per server.** Every plugin that uses it sets the same rule.
26
+
27
+ ## Installation
28
+
29
+ The server needs the ReSemiclip AMX Mod X module (`resemiclip_amxx`).
30
+
31
+ ```bash
32
+ npx amxts module add resemiclip
33
+ ```
34
+
35
+ It installs the package and adds it to your project's `amxts.config.ts`:
36
+
37
+ ```ts
38
+ export default defineConfig({
39
+ modules: ["@amxts/resemiclip"],
40
+ });
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ A plugin uses the module as `semiclip`, without an import line: the build adds the import to the plugins that use it, and builds the module only when some plugin does.
46
+
47
+ Teammates walk through each other:
48
+
49
+ ```ts
50
+ semiclip.rule = (player, target) => player.team == target.team;
51
+ ```
52
+
53
+ Setting the rule is all it takes: the module works every pair out, and works a player's pairs out again when he spawns, dies, changes sides, comes or leaves. Setting `rule` takes the rules over from ReSemiclip's config; `semiclip.rule = null` gives them back, and so does the plugins' reload or a map change until a rule is set again.
54
+
55
+ ### A rule that changes in play
56
+
57
+ `spawnProtected` below is the plugin's own [field on `Player`](https://amxts.github.io/docs/game/players#shared-player-fields), declared with `declare module`: it starts `false`, every plugin on the server reads and writes it, and it is cleared when the player leaves. A player who has just spawned walks through everyone alive for 3 seconds, and teammates walk through each other:
58
+
59
+ ```ts
60
+ declare module "~/facade" {
61
+ interface Player {
62
+ spawnProtected: boolean;
63
+ }
64
+ }
65
+
66
+ semiclip.rule = (player, target) =>
67
+ player.isAlive && target.isAlive && (player.spawnProtected || target.spawnProtected || player.team == target.team);
68
+
69
+ game.addEventListener("spawn", ({ player }) => {
70
+ player.spawnProtected = true; // walks through everyone for 3 seconds
71
+ setTimeout(() => (player.spawnProtected = false), 3000);
72
+ });
73
+ ```
74
+
75
+ The module works the pairs out again by itself when the field changes - on the spawn, and when the timer clears it - with no `update()` call.
76
+
77
+ ### When the pairs are worked out
78
+
79
+ | When | Which pairs |
80
+ | --- | --- |
81
+ | `rule` is set | every pair |
82
+ | a player comes into the game | his |
83
+ | a player spawns, dies or changes sides | his |
84
+ | a field plugins add to `Player` changes on a player - any plugin's, or Pawn's | his |
85
+ | a player leaves | nobody walks through his slot any more |
86
+ | `semiclip.update(player)` | `player`'s |
87
+ | `semiclip.update()` | every pair |
88
+
89
+ `update()` is for what the rule reads that the module does not hear - a round's mode kept in a variable, a plugin's own record of a player:
90
+
91
+ ```ts
92
+ let everyone = false;
93
+
94
+ semiclip.rule = (player, target) => everyone || player.team == target.team;
95
+
96
+ server.addCommand("/party", () => {
97
+ everyone = !everyone;
98
+ semiclip.update();
99
+ });
100
+ ```
101
+
102
+ ### Both ways
103
+
104
+ `rule(player, target)` says whether `player` walks through `target`. ReSemiclip lets two players through each other only when both ways say so, so a rule that says yes one way and no the other keeps that pair solid. A rule that reads the same both ways - `player.team == target.team`, `player.spawnProtected || target.spawnProtected` - is what makes a pair pass.
105
+
106
+ ### One rule
107
+
108
+ The rule is one for the server. A plugin that sets it over another plugin's replaces it, and the console warns: the last one set is used. Plugins that each have a say write it into fields (`player.spawnProtected`) that one rule reads.
109
+
110
+ ### Without ReSemiclip on the server
111
+
112
+ Nothing is set: the module does not take the rules, `passesThrough` gives no one, and the console says once that the resemiclip module is not on the server.
113
+
114
+ ## API
115
+
116
+ | Member | What it is |
117
+ | --- | --- |
118
+ | `semiclip.rule` | `(player, target) => boolean` - whether `player` walks through `target`. Setting it takes the rules over and works every pair out; `null` gives them back. |
119
+ | `semiclip.update(player?)` | Works `player`'s pairs out again, or everyone's, after something the rule reads changed that the module does not hear. |
120
+ | `semiclip.passesThrough(player)` | The players `player` walks through, as ReSemiclip has it. |
121
+
122
+ For Pawn plugins, ReSemiclip's own natives (`resemiclip_set_user_mask` and the rest) work as they are.
package/README.ru.md ADDED
@@ -0,0 +1,122 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.svg" width="96" alt="ReSemiclip">
4
+
5
+ # ReSemiclip
6
+
7
+ *Кто сквозь кого проходит — правилом над двумя игроками*
8
+
9
+ [![amxts module](https://img.shields.io/badge/amxts-module-3178c6?style=flat-square)](https://amxts.github.io/)
10
+ [![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
11
+
12
+ [Возможности](#возможности) • [Установка](#установка) • [Использование](#использование) • [API](#api)
13
+
14
+ [English](README.md) | **Русский**
15
+
16
+ </div>
17
+
18
+ Семиклип позволяет игрокам проходить сквозь друг друга. На сервере это делает модуль [ReSemiclip](https://github.com/Next21Team/resemiclip-amxx); этот модуль даёт плагину amxts сказать, кто сквозь кого проходит, правилом над двумя игроками, `(player, target) => boolean`, и держит ReSemiclip в курсе.
19
+
20
+ ## Возможности
21
+
22
+ - **Правило, а не битовые маски.** Одна функция говорит, проходит ли один игрок сквозь другого; модуль рассчитывает каждую пару и сообщает ReSemiclip.
23
+ - **Держится в курсе сам.** Появление, смерть, смена команды, вход и уход игрока и изменение любого поля, которое плагины добавили в `Player`, заново рассчитывают пары — только пары того игрока, с которым это случилось.
24
+ - **Об отсутствии модуля — один раз.** Без ReSemiclip на сервере ничего не задаётся, а консоль один раз говорит почему.
25
+ - **Один экземпляр на сервер.** Все плагины, которые им пользуются, задают одно и то же правило.
26
+
27
+ ## Установка
28
+
29
+ Серверу нужен модуль AMX Mod X ReSemiclip (`resemiclip_amxx`).
30
+
31
+ ```bash
32
+ npx amxts module add resemiclip
33
+ ```
34
+
35
+ Команда ставит пакет и добавляет его в `amxts.config.ts` проекта:
36
+
37
+ ```ts
38
+ export default defineConfig({
39
+ modules: ["@amxts/resemiclip"],
40
+ });
41
+ ```
42
+
43
+ ## Использование
44
+
45
+ Плагин пользуется модулем как `semiclip`, без строки импорта: сборка добавляет импорт в те плагины, которые им пользуются, и собирает модуль, только когда им пользуется хоть один.
46
+
47
+ Союзники проходят сквозь друг друга:
48
+
49
+ ```ts
50
+ semiclip.rule = (player, target) => player.team == target.team;
51
+ ```
52
+
53
+ Больше ничего не нужно: модуль рассчитывает каждую пару и заново рассчитывает пары игрока, когда тот появляется, умирает, меняет команду, входит или уходит. Присваивание `rule` забирает правила у конфига ReSemiclip; `semiclip.rule = null` возвращает их, и так же — перезагрузка плагинов или смена карты, пока правило не задано снова.
54
+
55
+ ### Правило, которое меняется в игре
56
+
57
+ `spawnProtected` ниже — собственное [поле плагина в `Player`](https://amxts.github.io/ru/docs/game/players#общие-поля-игрока), объявленное через `declare module`: сначала оно `false`, его читают и пишут все плагины сервера, а когда игрок уходит, оно сбрасывается. Только что появившийся игрок 3 секунды проходит сквозь всех живых, а союзники — сквозь друг друга:
58
+
59
+ ```ts
60
+ declare module "~/facade" {
61
+ interface Player {
62
+ spawnProtected: boolean;
63
+ }
64
+ }
65
+
66
+ semiclip.rule = (player, target) =>
67
+ player.isAlive && target.isAlive && (player.spawnProtected || target.spawnProtected || player.team == target.team);
68
+
69
+ game.addEventListener("spawn", ({ player }) => {
70
+ player.spawnProtected = true; // 3 секунды проходит сквозь всех
71
+ setTimeout(() => (player.spawnProtected = false), 3000);
72
+ });
73
+ ```
74
+
75
+ Модуль сам заново рассчитывает пары, когда поле меняется, — при появлении и когда таймер его снимает, — без вызова `update()`.
76
+
77
+ ### Когда пары рассчитываются
78
+
79
+ | Когда | Какие пары |
80
+ | --- | --- |
81
+ | задано `rule` | все пары |
82
+ | игрок вошёл в игру | его |
83
+ | игрок появился, умер или сменил команду | его |
84
+ | у игрока изменилось поле, которое плагины добавили в `Player`, — любым плагином или Pawn | его |
85
+ | игрок ушёл | сквозь его слот больше никто не проходит |
86
+ | `semiclip.update(player)` | пары `player` |
87
+ | `semiclip.update()` | все пары |
88
+
89
+ `update()` — для того, что читает правило, но чего модуль не слышит: режим раунда в переменной, собственная запись плагина об игроке:
90
+
91
+ ```ts
92
+ let everyone = false;
93
+
94
+ semiclip.rule = (player, target) => everyone || player.team == target.team;
95
+
96
+ server.addCommand("/party", () => {
97
+ everyone = !everyone;
98
+ semiclip.update();
99
+ });
100
+ ```
101
+
102
+ ### В обе стороны
103
+
104
+ `rule(player, target)` говорит, проходит ли `player` сквозь `target`. ReSemiclip пропускает двух игроков друг сквозь друга, только когда это разрешено в обе стороны, поэтому правило, которое в одну сторону говорит да, а в другую нет, оставляет пару твёрдой. Пару пропускает правило, которое читается одинаково в обе стороны, — `player.team == target.team`, `player.spawnProtected || target.spawnProtected`.
105
+
106
+ ### Одно правило
107
+
108
+ Правило на сервере одно. Плагин, который задаёт его поверх правила другого плагина, заменяет его, а консоль предупреждает: действует последнее заданное. Плагины, у каждого из которых есть своё слово, пишут его в поля (`player.spawnProtected`), которые читает одно правило.
109
+
110
+ ### Без ReSemiclip на сервере
111
+
112
+ Ничего не задаётся: модуль не забирает правила, `passesThrough` никого не даёт, а консоль один раз говорит, что модуля resemiclip на сервере нет.
113
+
114
+ ## API
115
+
116
+ | Член | Что это |
117
+ | --- | --- |
118
+ | `semiclip.rule` | `(player, target) => boolean` — проходит ли `player` сквозь `target`. Присваивание забирает правила и рассчитывает каждую пару; `null` возвращает их. |
119
+ | `semiclip.update(player?)` | Заново рассчитывает пары `player` или всех, после того как изменилось то, что читает правило и чего модуль не слышит. |
120
+ | `semiclip.passesThrough(player)` | Игроки, сквозь которых проходит `player`, как это сейчас у ReSemiclip. |
121
+
122
+ Pawn-плагинам собственные нативы ReSemiclip (`resemiclip_set_user_mask` и остальные) доступны как есть.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@amxts/resemiclip",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Who walks through whom, for amxts plugins: the ReSemiclip module's rules as players, not bit masks.",
6
+ "author": {
7
+ "name": "kukson777",
8
+ "url": "https://github.com/kukson777"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/amxts/resemiclip#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/amxts/resemiclip.git"
15
+ },
16
+ "keywords": [
17
+ "amxts-module",
18
+ "amxts-semiclip",
19
+ "amxmodx",
20
+ "counter-strike"
21
+ ],
22
+ "exports": {
23
+ ".": "./src/index.ts"
24
+ },
25
+ "types": "./src/index.ts",
26
+ "files": [
27
+ "README.md",
28
+ "README.ru.md",
29
+ "scripts/docs",
30
+ "src",
31
+ "prebuilt"
32
+ ],
33
+ "amxts": {
34
+ "module": "src/index.ts"
35
+ },
36
+ "scripts": {
37
+ "test": "bun test",
38
+ "check": "amxts check",
39
+ "lint": "oxlint && oxfmt --check",
40
+ "lint:fix": "oxfmt && oxlint --fix --silent || oxlint --fix --silent || oxlint --fix"
41
+ },
42
+ "peerDependencies": {
43
+ "@amxts/core": "^0.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@e18e/eslint-plugin": "^0.8.1",
47
+ "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
48
+ "@stylistic/eslint-plugin": "^5.10.0",
49
+ "@types/bun": "^1.4.2",
50
+ "eslint-plugin-antfu": "^3.2.3",
51
+ "eslint-plugin-command": "^4.0.0",
52
+ "eslint-plugin-n": "^18.4.0",
53
+ "eslint-plugin-perfectionist": "^5.12.1",
54
+ "eslint-plugin-regexp": "^3.3.1",
55
+ "oxfmt": "^0.70.0",
56
+ "oxlint": "^1.85.0",
57
+ "typescript": "^6.0.3"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
Binary file
@@ -0,0 +1,51 @@
1
+ {
2
+ "format": 1,
3
+ "module": "@amxts/resemiclip",
4
+ "version": "0.1.0",
5
+ "from": {
6
+ "@amxts/core": {
7
+ "version": "0.1.0",
8
+ "hash": "e3b12482fd1df1dcadee7e27a12ba915d97bf859"
9
+ },
10
+ "@amxts/resemiclip": {
11
+ "version": "0.1.0",
12
+ "hash": "788f84dd4fd47d0a1f7a96a5f4ad804a8f895045"
13
+ }
14
+ },
15
+ "options": {
16
+ "@amxts/resemiclip": {}
17
+ },
18
+ "forwards": {},
19
+ "places": [
20
+ "constants.ts",
21
+ "effects.ts",
22
+ "entities.ts",
23
+ "events.ts",
24
+ "facade.ts",
25
+ "flags.ts",
26
+ "hooks.ts",
27
+ "modules/resemiclip.ts",
28
+ "natives.ts",
29
+ "promise.ts",
30
+ "resemiclip.ts",
31
+ "vector.ts"
32
+ ],
33
+ "natives": [],
34
+ "beside": {
35
+ "enums": [],
36
+ "contract": null,
37
+ "author": null
38
+ },
39
+ "systems": {
40
+ "windows": {
41
+ "file": "prebuilt/windows/resemiclip.aot",
42
+ "size": 456628,
43
+ "sha256": "059a43c784b8efd29580300cec70128cacf59f88f2b086711ac02dc7c6ef22db"
44
+ },
45
+ "linux": {
46
+ "file": "prebuilt/linux/resemiclip.aot",
47
+ "size": 463288,
48
+ "sha256": "cbe969c8057a8f04b1b57a67548826b34d6c5bcf45d98523f8e049266a3cbbe1"
49
+ }
50
+ }
51
+ }
Binary file
@@ -0,0 +1,78 @@
1
+ // The tooltips of src/index.ts, in both languages: scripts/apply-docs.ts writes the
2
+ // one AMXTS_DOCS_LANG picks into the JSDoc above each element.
3
+ export default {
4
+ 'SemiclipRule': {
5
+ en: `
6
+ Whether \`player\` walks through \`target\`. ReSemiclip lets two players
7
+ through each other only when the rule says so both ways.
8
+ `,
9
+ ru: `
10
+ Проходит ли \`player\` сквозь \`target\`. ReSemiclip пропускает двух игроков
11
+ друг сквозь друга, только когда правило разрешает это в обе стороны.
12
+ `,
13
+ },
14
+ 'Semiclip': {
15
+ en: `
16
+ Who walks through whom, set as a rule over two players - \`semiclip\`, which
17
+ plugins use without an import:
18
+
19
+ \`\`\`ts
20
+ semiclip.rule = (player, target) => player.isAlive && target.isAlive && player.team == target.team;
21
+ \`\`\`
22
+ `,
23
+ ru: `
24
+ Кто сквозь кого проходит, заданное правилом над двумя игроками, — \`semiclip\`,
25
+ которым плагины пользуются без импорта:
26
+
27
+ \`\`\`ts
28
+ semiclip.rule = (player, target) => player.isAlive && target.isAlive && player.team == target.team;
29
+ \`\`\`
30
+ `,
31
+ },
32
+ 'Semiclip.rule': {
33
+ en: `
34
+ Whether \`player\` walks through \`target\`. Setting it takes the rules over
35
+ from ReSemiclip and works every pair out; \`null\` gives them back. The
36
+ pairs are worked out again when a player comes or leaves, spawns, dies or
37
+ changes sides, and when a field plugins added to \`Player\` changes.
38
+
39
+ Pawn: \`resemiclip_take_control\`, \`resemiclip_set_user_mask\`
40
+ `,
41
+ ru: `
42
+ Проходит ли \`player\` сквозь \`target\`. Присваивание забирает правила у
43
+ ReSemiclip и рассчитывает каждую пару; \`null\` возвращает их. Пары
44
+ рассчитываются заново, когда игрок заходит или уходит, появляется, умирает
45
+ или меняет команду и когда меняется поле, которое плагины добавили в \`Player\`.
46
+
47
+ Pawn: \`resemiclip_take_control\`, \`resemiclip_set_user_mask\`
48
+ `,
49
+ },
50
+ 'Semiclip.update': {
51
+ en: `
52
+ Works the pairs out again - \`player\`'s, or everyone's - after something
53
+ the rule reads changed that the module does not hear: a round's mode, a
54
+ plugin's own record.
55
+ `,
56
+ ru: `
57
+ Рассчитывает пары заново — пары \`player\` или всех — после того, как
58
+ изменилось то, что читает правило и чего модуль не слышит: режим раунда,
59
+ собственная запись плагина.
60
+ `,
61
+ },
62
+ 'Semiclip.passesThrough': {
63
+ en: `
64
+ The players \`player\` walks through: those he is not solid to.
65
+
66
+ Pawn: \`resemiclip_get_user_mask\`
67
+ `,
68
+ ru: `
69
+ Игроки, сквозь которых проходит \`player\`: те, для кого он не твёрдый.
70
+
71
+ Pawn: \`resemiclip_get_user_mask\`
72
+ `,
73
+ },
74
+ 'semiclip': {
75
+ en: `Who walks through whom: the rule, the pairs worked out again, and what ReSemiclip has.`,
76
+ ru: `Кто сквозь кого проходит: правило, повторный расчёт пар и то, что сейчас у ReSemiclip.`,
77
+ },
78
+ };
package/src/index.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * ReSemiclip for amxts plugins: who walks through whom, as a rule over two
3
+ * players. The ReSemiclip module keeps, for each player, the players he is not
4
+ * solid to; this module asks the rule and tells it, again whenever what the
5
+ * rule reads may have changed. How to use it: README.md.
6
+ */
7
+ import { ClientPutinserverEvent, Player, TeamInfoMessage, game, server } from "@amxts/core";
8
+ import { LibType_Library } from "~/constants";
9
+ import { LibraryExists, resemiclip_get_user_mask, resemiclip_set_user_mask, resemiclip_take_control } from "~/natives";
10
+
11
+ export default defineModule({
12
+ meta: { name: "resemiclip" },
13
+ imports: [{ from: "@amxts/resemiclip", name: "semiclip" }],
14
+ setup() {
15
+ server.addEventListener("init", start);
16
+ server.addEventListener("putinserver", joined);
17
+ server.addEventListener("disconnected", event => left(event.player));
18
+ server.addEventListener("message:TeamInfo", teamChanged);
19
+ server.addEventListener("playerchange", event => changed(event.player));
20
+ game.addEventListener("spawn", event => changed(event.player), true);
21
+ game.addEventListener("killed", event => changed(event.player), true);
22
+ },
23
+ });
24
+
25
+ /**
26
+ * Whether `player` walks through `target`. ReSemiclip lets two players
27
+ * through each other only when the rule says so both ways.
28
+ */
29
+ export type SemiclipRule = (player: Player, target: Player) => boolean;
30
+
31
+ let current: SemiclipRule | null = null;
32
+
33
+ /** The server is up. A rule set while the plugins are still loading waits for it. */
34
+ let started = false;
35
+
36
+ /** What ReSemiclip was told, by player: bit `N` - player `N + 1` is walked through. */
37
+ const masks = new Map<number, number>();
38
+
39
+ // Asked once: the ReSemiclip module is either on the server for the map or not.
40
+ let answered = false;
41
+ let present = false;
42
+
43
+ /** Whether the ReSemiclip module is loaded; the first time it is not, the console says so. */
44
+ function loaded() {
45
+ if (answered) return present;
46
+ answered = true;
47
+ present = LibraryExists("resemiclip", LibType_Library) != 0;
48
+ if (!present) console.error("[ReSemiclip] the resemiclip module is not on the server: semiclip does nothing");
49
+ return present;
50
+ }
51
+
52
+ /**
53
+ * Who walks through whom, set as a rule over two players - `semiclip`, which
54
+ * plugins use without an import:
55
+ *
56
+ * ```ts
57
+ * semiclip.rule = (player, target) => player.isAlive && target.isAlive && player.team == target.team;
58
+ * ```
59
+ */
60
+ export class Semiclip {
61
+ /**
62
+ * Whether `player` walks through `target`. Setting it takes the rules over
63
+ * from ReSemiclip and works every pair out; `null` gives them back. The
64
+ * pairs are worked out again when a player comes or leaves, spawns, dies or
65
+ * changes sides, and when a field plugins added to `Player` changes.
66
+ *
67
+ * Pawn: `resemiclip_take_control`, `resemiclip_set_user_mask`
68
+ */
69
+ get rule() {
70
+ return current;
71
+ }
72
+
73
+ set rule(rule: SemiclipRule | null) {
74
+ if (rule != null && current != null && rule != current) console.warn("[ReSemiclip] a rule was set over another one: the last one set is used");
75
+ current = rule;
76
+ if (started) apply();
77
+ }
78
+
79
+ /**
80
+ * Works the pairs out again - `player`'s, or everyone's - after something
81
+ * the rule reads changed that the module does not hear: a round's mode, a
82
+ * plugin's own record.
83
+ */
84
+ update(player?: Player) {
85
+ const rule = activeRule();
86
+ if (rule == null) return;
87
+ if (player) updatePlayer(player, rule);
88
+ else updateAll(rule);
89
+ }
90
+
91
+ /**
92
+ * The players `player` walks through: those he is not solid to.
93
+ *
94
+ * Pawn: `resemiclip_get_user_mask`
95
+ */
96
+ passesThrough(player: Player) {
97
+ const mask = loaded() ? resemiclip_get_user_mask(player.id) : 0;
98
+ return Player.all().filter(other => other.id != player.id && (mask & bitOf(other)) != 0);
99
+ }
100
+ }
101
+
102
+ /** Who walks through whom: the rule, the pairs worked out again, and what ReSemiclip has. */
103
+ export const semiclip = new Semiclip();
104
+
105
+ function start() {
106
+ started = true;
107
+ apply();
108
+ }
109
+
110
+ /** Takes the rules from ReSemiclip for the rule, or gives them back, and works every pair out. */
111
+ function apply() {
112
+ if (!loaded()) return;
113
+ masks.clear();
114
+ const rule = current;
115
+ resemiclip_take_control(rule != null);
116
+ if (rule != null) updateAll(rule);
117
+ }
118
+
119
+ /** The rule, while the module sets the pairs: the server is up, ReSemiclip is there and a rule is set. */
120
+ function activeRule() {
121
+ return started && present ? current : null;
122
+ }
123
+
124
+ function joined(event: ClientPutinserverEvent) {
125
+ const player = new Player(event.player.id);
126
+ changed(player);
127
+ }
128
+
129
+ /** A team change is sent to everyone; a TeamInfo to one player only brings him up to date. */
130
+ function teamChanged(event: TeamInfoMessage) {
131
+ const target = event.target;
132
+ if (event.player == null && target != null) changed(target);
133
+ }
134
+
135
+ /** Something the rule may read changed on `player`: his pairs, both ways. */
136
+ function changed(player: Player) {
137
+ const rule = activeRule();
138
+ if (rule != null) updatePlayer(player, rule);
139
+ }
140
+
141
+ /** `player` left: nobody walks through his slot, and it is forgotten for the next to take it. */
142
+ function left(player: Player) {
143
+ masks.delete(player.id);
144
+ if (activeRule() == null) return;
145
+ for (const other of Player.all()) {
146
+ if (other.id != player.id) tell(other, withBit(maskOf(other), player, false));
147
+ }
148
+ }
149
+
150
+ function updateAll(rule: SemiclipRule) {
151
+ const players = Player.all();
152
+ for (const player of players) tell(player, maskFor(players.filter(other => other.id != player.id && rule(player, other))));
153
+ }
154
+
155
+ function updatePlayer(player: Player, rule: SemiclipRule) {
156
+ const others = Player.all().filter(other => other.id != player.id);
157
+ tell(player, maskFor(others.filter(other => rule(player, other))));
158
+ for (const other of others) tell(other, withBit(maskOf(other), player, rule(other, player)));
159
+ }
160
+
161
+ /** Tells ReSemiclip whom `player` walks through, when that changed. */
162
+ function tell(player: Player, mask: number) {
163
+ if (masks.has(player.id) && masks.get(player.id) == mask) return;
164
+ masks.set(player.id, mask);
165
+ resemiclip_set_user_mask(player.id, mask);
166
+ }
167
+
168
+ /** What ReSemiclip was told for `player`. */
169
+ function maskOf(player: Player) {
170
+ return masks.has(player.id) ? masks.get(player.id) : 0;
171
+ }
172
+
173
+ /** The module's mask for these players: a bit each. */
174
+ function maskFor(players: Player[]) {
175
+ let mask = 0;
176
+ for (const one of players) mask = mask | bitOf(one);
177
+ return mask;
178
+ }
179
+
180
+ /** `mask` with `player`'s bit on or off. */
181
+ function withBit(mask: number, player: Player, on: boolean) {
182
+ return on ? mask | bitOf(player) : mask & ~bitOf(player);
183
+ }
184
+
185
+ /** The module's bit for a player: one per slot, 1 << (id - 1). */
186
+ function bitOf(player: Player) {
187
+ return 1 << (player.id - 1);
188
+ }