@amxts/menu-core 0.2.1 → 0.3.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/PAWN.md +2 -2
- package/PAWN.ru.md +2 -2
- package/README.md +8 -5
- package/README.ru.md +8 -5
- package/package.json +3 -3
- package/prebuilt/linux/menu-core.aot +0 -0
- package/prebuilt/manifest.json +36 -35
- package/prebuilt/windows/menu-core.aot +0 -0
- package/scripts/docs/src/menus.ts +18 -2
- package/scripts/docs/src/types.ts +4 -4
- package/src/internal.ts +4 -0
- package/src/menu-file.ts +13 -5
- package/src/menus.ts +54 -15
- package/src/natives.ts +411 -411
- package/src/types.ts +4 -4
package/src/menu-file.ts
CHANGED
|
@@ -597,7 +597,7 @@ function iniMenu(section: ConfigNode) {
|
|
|
597
597
|
const content = section.has("ITEMS") || section.has("VIEW") || section.has("FIXED_ITEMS");
|
|
598
598
|
|
|
599
599
|
if (title.length == 0) {
|
|
600
|
-
|
|
600
|
+
warn(whereOf(section), content ? `[${name}] has no TITLE, so it is not a menu` : `[${name}] is not a menu: it has no TITLE, ITEMS or VIEW`);
|
|
601
601
|
return null;
|
|
602
602
|
}
|
|
603
603
|
|
|
@@ -633,12 +633,20 @@ function iniMenu(section: ConfigNode) {
|
|
|
633
633
|
return withItems(menu, whereOf(section));
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
|
|
636
|
+
// The Pawn menu_core reads its words from [Основное]: a file written for it is read as it is.
|
|
637
|
+
const INI_MAIN_SECTIONS = ["MAIN", "Основное"];
|
|
638
|
+
|
|
639
|
+
/** Whether a section of an INI menu file is the one with the words. */
|
|
640
|
+
function isMainSection(section: ConfigNode) {
|
|
641
|
+
return INI_MAIN_SECTIONS.includes(section.key) || section.key.toUpperCase() == "MAIN";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** An INI menu file: [MAIN] (or the Pawn menu_core's [Основное]) with the words, a [section] a menu. */
|
|
637
645
|
function readIni(root: ConfigNode, file: MenuFile) {
|
|
638
|
-
const main = root.
|
|
646
|
+
const main = root.values().find(isMainSection);
|
|
639
647
|
|
|
640
648
|
if (main != null && main.kind == "object") {
|
|
641
|
-
checkKeys(main, INI_MAIN_KEYS,
|
|
649
|
+
checkKeys(main, INI_MAIN_KEYS, `[${main.key}]`, true);
|
|
642
650
|
file.labels.prefix = fileText(first(main.get("PREFIX")), whereOf(main));
|
|
643
651
|
const key = main.get("KEY");
|
|
644
652
|
if (key != null && key.kind == "object") checkKeys(key, INI_LABEL_KEYS, "KEY", true);
|
|
@@ -646,7 +654,7 @@ function readIni(root: ConfigNode, file: MenuFile) {
|
|
|
646
654
|
}
|
|
647
655
|
|
|
648
656
|
for (const section of root.values()) {
|
|
649
|
-
if (section
|
|
657
|
+
if (isMainSection(section) || section.kind != "object") continue;
|
|
650
658
|
const menu = iniMenu(section);
|
|
651
659
|
if (menu != null) file.menus.push(menu);
|
|
652
660
|
}
|
package/src/menus.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Menu Core's menus: the Menu object, the functions of the module, and how a
|
|
3
3
|
* menu is drawn and chosen from.
|
|
4
4
|
*/
|
|
5
|
-
import { accessOf, Forward, Player, clearInterval, lang,
|
|
5
|
+
import { accessOf, Forward, Player, clearInterval, lang, server, setInterval, setTimeout } from "@amxts/core";
|
|
6
6
|
import { callingPlugin, onPluginStop, publicFor, showMenu } from "@amxts/core/kit";
|
|
7
7
|
import { GetLangTransKey, get_maxplayers, register_menucmd, register_menuid } from "@amxts/core/natives";
|
|
8
8
|
import { ActionHandler, ActionTest, ConditionFilter, ConditionTest, ListRow, ListSource, MenuContext, MenuEventType, MenuItemOptions, MenuKind, MenuOptions, MenuShowOptions, NamedContext, PlaceholderValue, RestrictionTest, RowTest } from "./types";
|
|
@@ -207,7 +207,9 @@ const PAGE_SLOTS = 7;
|
|
|
207
207
|
const MAX_DEPTH = 5;
|
|
208
208
|
const ALL_KEYS = 1023;
|
|
209
209
|
/** The conditions Menu Core answers itself while nobody registered them - TEAM_<team> and FLAG_<letters> besides. */
|
|
210
|
-
const BUILT_IN_CONDITIONS = ["IS_ALIVE", "IS_DEAD", "IS_BOT", "IS_ADMIN"];
|
|
210
|
+
const BUILT_IN_CONDITIONS = ["IS_ALIVE", "IS_DEAD", "IS_BOT", "IS_ADMIN", "ADMIN", "ACCESS_ADMIN"];
|
|
211
|
+
/** ADMIN and ACCESS_ADMIN, as the Pawn menu_core answered them: ban, rcon, admin or menu access. */
|
|
212
|
+
const ADMIN_FLAGS = "FLAG_dluy";
|
|
211
213
|
/** The teams of TEAM_<team>: the names of `player.team`. */
|
|
212
214
|
const TEAMS = ["CT", "TERRORIST", "SPECTATOR", "UNASSIGNED"];
|
|
213
215
|
const KEY_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
|
@@ -272,6 +274,7 @@ server.addEventListener("putInServer", (event) => {
|
|
|
272
274
|
viewer.menu = "";
|
|
273
275
|
viewer.history = [];
|
|
274
276
|
viewer.page = 0;
|
|
277
|
+
viewer.nextPage = -1;
|
|
275
278
|
viewer.locked = false;
|
|
276
279
|
});
|
|
277
280
|
|
|
@@ -310,8 +313,18 @@ export function menuAt(index: number) {
|
|
|
310
313
|
return find(menus[index].name);
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
/**
|
|
314
|
-
|
|
316
|
+
/**
|
|
317
|
+
* The menus of those names in the menu file, read now if they are not yet:
|
|
318
|
+
* `menus.register("SHOP", "SHOP_GUNS")`. Returns the first one's menu;
|
|
319
|
+
* `null` when the file has no such menu, or no items in it.
|
|
320
|
+
*/
|
|
321
|
+
export function register(...names: string[]) {
|
|
322
|
+
const found = names.map(registerOne);
|
|
323
|
+
return found.length > 0 ? found[0] : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The menu of that name in the menu file, read now if it is not yet; `null` when the file has none. */
|
|
327
|
+
function registerOne(name: string) {
|
|
315
328
|
const known = find(name);
|
|
316
329
|
if (known != null) return known;
|
|
317
330
|
const spec = loadedFile().menus.find(each => each.name == name);
|
|
@@ -388,6 +401,16 @@ export function addAction(name: string, run: ActionHandler) {
|
|
|
388
401
|
return actions.length - 1;
|
|
389
402
|
}
|
|
390
403
|
|
|
404
|
+
/** Registers actions by name, for menu files and Pawn plugins: `menus.addActions({ SHOP_HEAL: heal, SHOP_ARMOR: (player) => player.armor = 100 })`. */
|
|
405
|
+
export function addActions(actions: Record<string, ActionHandler>) {
|
|
406
|
+
for (const [name, run] of Object.entries(actions)) addAction(name, run);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Registers placeholders for menu files and Pawn plugins: `menus.addPlaceholders({ nick: (player) => player.name })`. A name registered twice keeps the first. */
|
|
410
|
+
export function addPlaceholders(placeholders: Record<string, PlaceholderValue>) {
|
|
411
|
+
for (const [name, value] of Object.entries(placeholders)) addPlaceholder(name, value);
|
|
412
|
+
}
|
|
413
|
+
|
|
391
414
|
/** Registers a placeholder for menu files and Pawn plugins: the text `%name%` stands for in titles and items. A name registered twice keeps the first. In code the text is a function instead. */
|
|
392
415
|
export function addPlaceholder(name: string, value: PlaceholderValue) {
|
|
393
416
|
const known = placeholders.findIndex(entry => entry.name == name);
|
|
@@ -453,11 +476,18 @@ export function textRow(text: string, centered = false) {
|
|
|
453
476
|
* it is not active, or the player's menu holds on.
|
|
454
477
|
*/
|
|
455
478
|
export function show(player: Player, name: string, options: MenuShowOptions = {}) {
|
|
456
|
-
|
|
479
|
+
const viewer = viewerOf(player.id);
|
|
480
|
+
// The menu he is on, shown from anywhere but its own item - a command, a
|
|
481
|
+
// timer - opens anew, at its first page; from its item it stays on its page.
|
|
482
|
+
const anew = viewer.menu == name && viewer.choosing != name;
|
|
483
|
+
return open(player, name, options, options.target?.id ?? 0, anew);
|
|
457
484
|
}
|
|
458
485
|
|
|
459
|
-
/**
|
|
460
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Shows a menu about `target`, a number: a player's `id`, or what a Pawn
|
|
488
|
+
* plugin gave - `0` for none. `anew`: the menu he is on starts at its first page.
|
|
489
|
+
*/
|
|
490
|
+
function open(player: Player, name: string, options: MenuShowOptions, target: number, anew = false) {
|
|
461
491
|
if (!player.isConnected) return false;
|
|
462
492
|
const viewer = viewerOf(player.id);
|
|
463
493
|
const menu = menuNamed(name);
|
|
@@ -485,7 +515,7 @@ function open(player: Player, name: string, options: MenuShowOptions, target: nu
|
|
|
485
515
|
|
|
486
516
|
if (viewer.depth >= MAX_DEPTH) return false;
|
|
487
517
|
viewer.depth++;
|
|
488
|
-
const shown = draw(player, viewer, menu, options, target);
|
|
518
|
+
const shown = draw(player, viewer, menu, options, target, anew);
|
|
489
519
|
viewer.depth--;
|
|
490
520
|
return shown;
|
|
491
521
|
}
|
|
@@ -558,7 +588,7 @@ export function isLocked(player: Player) {
|
|
|
558
588
|
|
|
559
589
|
/** Sets the page the player's menu is drawn at next, from `0`. */
|
|
560
590
|
export function setPage(player: Player, page: number) {
|
|
561
|
-
viewerOf(player.id).
|
|
591
|
+
viewerOf(player.id).nextPage = page;
|
|
562
592
|
}
|
|
563
593
|
|
|
564
594
|
/** Whether an action of that name is registered. */
|
|
@@ -593,7 +623,8 @@ function contextOf(player: Player, target: number, menu: Menu) {
|
|
|
593
623
|
/** The context a function registered by name gets: the menu's, and the name it is asked by. */
|
|
594
624
|
function namedContextOf(player: Player, target: number, menu: Menu, name: string) {
|
|
595
625
|
const context: NamedContext = { player, target: playerOr(target, player), row: target, menu, name };
|
|
596
|
-
|
|
626
|
+
// What an action or a placeholder takes: the context, or its player for a function of the player - the compiler hands it that.
|
|
627
|
+
return context as NamedContext & Player;
|
|
597
628
|
}
|
|
598
629
|
|
|
599
630
|
/** The player a target numbers; `fallback` for none. */
|
|
@@ -953,7 +984,7 @@ function usesCondition(menu: Menu, name: string) {
|
|
|
953
984
|
|
|
954
985
|
function viewerOf(id: number) {
|
|
955
986
|
if (!viewers.has(id)) {
|
|
956
|
-
const made: Viewer = { menu: "", page: 0, target: 0, history: [], slots: [], rows: 0, text: "", locked: false, timer: 0, ticker: 0, depth: 0 };
|
|
987
|
+
const made: Viewer = { menu: "", page: 0, nextPage: -1, choosing: "", target: 0, history: [], slots: [], rows: 0, text: "", locked: false, timer: 0, ticker: 0, depth: 0 };
|
|
957
988
|
viewers.set(id, made);
|
|
958
989
|
}
|
|
959
990
|
|
|
@@ -966,7 +997,7 @@ function menuOf(viewer: Viewer) {
|
|
|
966
997
|
}
|
|
967
998
|
|
|
968
999
|
function menuNamed(name: string) {
|
|
969
|
-
return find(name) ??
|
|
1000
|
+
return find(name) ?? registerOne(name);
|
|
970
1001
|
}
|
|
971
1002
|
|
|
972
1003
|
function lookingAt(menu: Menu) {
|
|
@@ -1002,7 +1033,7 @@ function isVisible(item: MenuItem, player: Player, target: number, menu: Menu) {
|
|
|
1002
1033
|
return check == null || passes(check, player, player.id, target, menu);
|
|
1003
1034
|
}
|
|
1004
1035
|
|
|
1005
|
-
function draw(player: Player, viewer: Viewer, menu: Menu, options: MenuShowOptions, target: number) {
|
|
1036
|
+
function draw(player: Player, viewer: Viewer, menu: Menu, options: MenuShowOptions, target: number, anew: boolean) {
|
|
1006
1037
|
// A list menu nobody is left in does not open, and nothing of the viewer's changes.
|
|
1007
1038
|
const listing = menu.kind == "list" ? listOf(player, menu, target) : noListing();
|
|
1008
1039
|
|
|
@@ -1039,7 +1070,12 @@ function draw(player: Player, viewer: Viewer, menu: Menu, options: MenuShowOptio
|
|
|
1039
1070
|
if (!given) timer = menu.countdown > 0 ? menu.countdown : viewer.timer;
|
|
1040
1071
|
if (timer <= 0) timer = menu.time;
|
|
1041
1072
|
|
|
1042
|
-
if (
|
|
1073
|
+
if (viewer.nextPage >= 0) {
|
|
1074
|
+
viewer.page = viewer.nextPage;
|
|
1075
|
+
viewer.nextPage = -1;
|
|
1076
|
+
} else if (anew || (!returning && !options.skipHistory && back < 0)) {
|
|
1077
|
+
viewer.page = 0;
|
|
1078
|
+
}
|
|
1043
1079
|
|
|
1044
1080
|
const items = menu.kind == "list" ? noItems() : shownItems(player, viewer, menu);
|
|
1045
1081
|
const total = menu.kind == "list" ? listing.count : items.length;
|
|
@@ -1412,7 +1448,7 @@ function sayEmpty(player: Player, menu: Menu) {
|
|
|
1412
1448
|
}
|
|
1413
1449
|
|
|
1414
1450
|
function say(player: Player, message: string) {
|
|
1415
|
-
print(
|
|
1451
|
+
player.print(`${ui(player.id, labels.prefix, DEFAULTS.prefix)} ${translate(player.id, message)}`);
|
|
1416
1452
|
}
|
|
1417
1453
|
|
|
1418
1454
|
function pressed(player: Player, key: number) {
|
|
@@ -1442,7 +1478,9 @@ function pressed(player: Player, key: number) {
|
|
|
1442
1478
|
|
|
1443
1479
|
const slot = viewer.slots[key];
|
|
1444
1480
|
if (slot.action.length == 0) return;
|
|
1481
|
+
viewer.choosing = menu.name;
|
|
1445
1482
|
run(player, slot.action, slot.target, menu);
|
|
1483
|
+
viewer.choosing = "";
|
|
1446
1484
|
if (viewer.menu == menu.name) open(player, menu.name, {}, viewer.target);
|
|
1447
1485
|
}
|
|
1448
1486
|
|
|
@@ -1569,6 +1607,7 @@ function builtInHolds(player: Player, name: string) {
|
|
|
1569
1607
|
if (upper == "IS_DEAD") return !player.isAlive;
|
|
1570
1608
|
if (upper == "IS_BOT") return player.isBot;
|
|
1571
1609
|
if (upper == "IS_ADMIN") return isAdmin(player);
|
|
1610
|
+
if (upper == "ADMIN" || upper == "ACCESS_ADMIN") return hasAccess(player, ADMIN_FLAGS);
|
|
1572
1611
|
if (upper.startsWith("TEAM_")) return player.team == upper.slice(5);
|
|
1573
1612
|
return hasAccess(player, name);
|
|
1574
1613
|
}
|