@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/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
- if (content) warn(whereOf(section), `[${name}] has no TITLE, so it is not a menu`);
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
- /** An INI menu file: [MAIN] with the words, a [section] a menu. */
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.get("MAIN");
646
+ const main = root.values().find(isMainSection);
639
647
 
640
648
  if (main != null && main.kind == "object") {
641
- checkKeys(main, INI_MAIN_KEYS, "[MAIN]", true);
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.key.toUpperCase() == "MAIN" || section.kind != "object") continue;
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, print, server, setInterval, setTimeout } from "@amxts/core";
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
- /** The menu of that name in the menu file, read now if it is not yet; `null` when the file has no such menu, or no items in it. */
314
- export function register(name: string) {
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
- return open(player, name, options, options.target?.id ?? 0);
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
- /** Shows a menu about `target`, a number: a player's `id`, or what a Pawn plugin gave - `0` for none. */
460
- function open(player: Player, name: string, options: MenuShowOptions, target: number) {
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).page = page;
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
- return context;
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) ?? register(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 (!returning && !options.skipHistory && back < 0) viewer.page = 0;
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(player, `${ui(player.id, labels.prefix, DEFAULTS.prefix)} ${translate(player.id, message)}`);
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
  }