├── .gitattributes
├── .github
└── workflows
│ ├── pr-trusted.yml
│ └── pr-untrusted.yml
├── .gitignore
├── .vscode
└── tasks.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── archived
└── map-vote.js
├── build
├── LICENSE
├── README.md
├── mod.hjson
└── scripts
│ ├── api.js
│ ├── commands.js
│ ├── config.js
│ ├── consoleCommands.js
│ ├── files.js
│ ├── fjsContext.js
│ ├── funcs.js
│ ├── globals.js
│ ├── index.js
│ ├── io.js
│ ├── main.js
│ ├── maps.js
│ ├── memberCommands.js
│ ├── menus.js
│ ├── metrics.js
│ ├── mindustryTypes.js
│ ├── packetHandlers.js
│ ├── playerCommands.js
│ ├── players.js
│ ├── promise.js
│ ├── ranks.js
│ ├── staffCommands.js
│ ├── timers.js
│ ├── types.js
│ ├── utils.js
│ └── votes.js
├── copy-client.js
├── docs
├── fail.png
├── info.md
├── intellisense.png
├── menus.png
└── race-condition.png
├── mod.hjson
├── package.json
├── pnpm-lock.yaml
├── scripts
├── attach.js
├── attach.ts
├── build.js
├── build.ts
├── dev.js
├── dev.ts
├── main.js
└── tsconfig.json
├── spec
├── src
│ ├── env.ts
│ ├── test-utils.ts
│ └── utils.spec.ts
├── support
│ └── jasmine.json
└── tsconfig.json
├── src
├── api.ts
├── client.$ts
├── commands.ts
├── config.ts
├── consoleCommands.ts
├── files.ts
├── fjsContext.ts
├── funcs.ts
├── globals.ts
├── index.ts
├── io.ts
├── main.js
├── maps.ts
├── memberCommands.ts
├── menus.ts
├── metrics.ts
├── mindustry.d.ts
├── mindustryTypes.ts
├── packetHandlers.ts
├── playerCommands.ts
├── players.ts
├── promise.ts
├── ranks.ts
├── rhino-env.d.ts
├── staffCommands.ts
├── timers.ts
├── types.ts
├── utils.ts
└── votes.ts
└── tsconfig.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | build/scripts/*.js linguist-generated
2 |
--------------------------------------------------------------------------------
/.github/workflows/pr-trusted.yml:
--------------------------------------------------------------------------------
1 | name: Typescript Compile and Tests
2 | on:
3 | # This event type is dangerous
4 | pull_request_target:
5 | types: [opened, reopened, synchronize]
6 | paths:
7 | - 'src/**.ts'
8 | branches:
9 | - 'master'
10 | jobs:
11 | Typescript_Compile_and_Tests:
12 | # Ensure the PR is not from a fork
13 | if: github.event.pull_request.head.repo.full_name == github.repository
14 | # PR is from the base repository, so it is only running code that was added by someone with write access, so it's trusted
15 | runs-on: ubuntu-latest
16 | env:
17 | # for some reason this isn't the bot ID, it's some other random number
18 | FISH_BOT_ACTOR_ID: 196413533
19 | permissions:
20 | contents: write
21 | steps:
22 | # Create a token that can be used to push commits
23 | - uses: actions/create-github-app-token@v1
24 | id: generate-token
25 | with:
26 | app-id: ${{ secrets.FISH_BOT_ID }}
27 | private-key: ${{ secrets.FISH_BOT_PRIVATE_KEY }}
28 | - uses: actions/setup-node@v4
29 | with:
30 | node-version: '22.x'
31 | # Checkout the trusted code
32 | - uses: actions/checkout@v1
33 | with:
34 | ref: ${{ github.head_ref }}
35 | - name: Git Config
36 | run: |
37 | git config --global core.autocrlf true
38 | git config --global user.name 'github-actions[bot]'
39 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
40 | git remote set-url origin https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com/${{ github.repository }}
41 | - name: Install pnpm
42 | uses: pnpm/action-setup@v4
43 | with:
44 | version: 9
45 | run_install: true
46 | - name: Typescript Compile
47 | # Only run this if the commit wasn't created by the fish bot
48 | # If it was, this check has already been completed
49 | if: github.actor_id != env.FISH_BOT_ACTOR_ID
50 | run: pnpm run build
51 | - name: Check for modified files in build directory
52 | id: git-check
53 | # Returns 0 if files changed, and 1 if no files changed
54 | run: |
55 | git add build/scripts
56 | echo "committed=$(git commit -m "Automated TypeScript compile" > /dev/null; echo $?)" >> $GITHUB_OUTPUT
57 | - name: Push changes
58 | # Do not create an infinite loop of workflows
59 | if: github.actor_id != env.FISH_BOT_ACTOR_ID && steps.git-check.outputs.committed == '0'
60 | run: git push
61 | - name: Run Tests
62 | if: github.actor_id == env.FISH_BOT_ACTOR_ID || steps.git-check.outputs.committed != '0'
63 | run: pnpm run test
64 |
--------------------------------------------------------------------------------
/.github/workflows/pr-untrusted.yml:
--------------------------------------------------------------------------------
1 | name: PR Tests (Untrusted)
2 | on:
3 | pull_request:
4 | types: [opened, reopened, synchronize]
5 | paths:
6 | - 'src/**.ts'
7 | branches:
8 | - 'master'
9 | jobs:
10 | PR_Tests:
11 | # Ensure the PR IS from a fork: otherwise, pr-trusted will run
12 | if: github.event.pull_request.head.repo.full_name != github.repository
13 | # It's safe to run tests if the PR is from a fork
14 | # This workflow doesn't have write permissions
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: read
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Install pnpm
21 | uses: pnpm/action-setup@v4
22 | with:
23 | version: 9
24 | run_install: true
25 | - name: Run Tests
26 | # this also runs tsc
27 | run: pnpm run test
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | asd
2 | node_modules
3 | package-lock.json
4 | ts-debugtrace
5 | tsconfig.tsbuildinfo
6 | build/scripts/*.d.ts
7 | spec/build
8 | scripts/client.js
9 | scripts/client.ts
10 | dev-server
11 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "watch",
7 | "group": "build",
8 | "problemMatcher": [],
9 | "label": "tsc: watch",
10 | "detail": "tsc --watch",
11 | "runOptions": {
12 | "runOn": "folderOpen"
13 | },
14 | "presentation": {
15 | "focus": false,
16 | "panel": "dedicated",
17 | "clear": false,
18 | "close": false,
19 | }
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing
3 |
4 | What kind of changes do you want to make?
5 |
6 | ## I want to make small changes (just change some text)
7 |
8 | You can make these changes entirely through the Github website. You will need a Github account.
9 |
10 | 1. Open the file that you want to edit. If you are unsure of which file to edit, try using Github's search feature.
11 | 2. Click the edit icon in the toolbar. If Github tells you to create a fork, do that.
12 | 3. Make your change, then click "Commit changes..."
13 | 4. Write a message, then click Propose changes.
14 | - If you are a member of the Fish-Community org, you will see a message about branch protection. Select "Create a new branch and start a pull request".
15 | - If you are not a member, you will also need to manually edit the corresponding .js file, which is located in `build/scripts`. For example, if you edited `src/config.ts`, you will need to make the same change to `build/scripts/config.js`.
16 | 5. Create a pull request and request a review. If you see a message about failing checks, there may be a problem with your changes.
17 |
18 | ## I want to make significant code changes (in VSCode)
19 |
20 | First: read [info.md](docs/info.md)
21 |
22 | To get started with development:
23 | 1. Create a fork of fish-commands through the GitHub website.
24 | 2. Clone that fork into a folder of your choice by `cd`ing into the folder and running `git clone https://github.com/`(your username here)`/fish-commands .`
25 | 3. Run `npm install`
26 | 4. Run `npm watch` in one terminal. (Or, open VS Code and tell it to trust the project)
27 | 5. In another terminal, run `npm dev` to start up a Mindustry development server with fish-commands installed.
28 |
29 | ### Making changes
30 | 1. Edit the code, which is in `/src`.
31 | 2. Restart the development server. Close it by pressing Ctrl+C or typing `exit`.
32 | 3. Test your changes.
33 |
34 | To use fish-commands with a server installed somewhere else, run `npm attach [jarfilepath.jar]`
35 |
36 | Once you have made and tested your changes, submit a PR:
37 | 1. Commit your changes to Git by running `git add . && git commit -m "`(description of your changes here)`"`
38 | 2. Upload your changes by running `git push`. 3. You should see a link to create the PR. If not, create one through Github.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright © BalaM314, 2025. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fish commands
2 |
3 | A monolithic plugin that handles all custom features for the >|||>Fish servers. Created by Brandons404, rewritten by BalaM314.
4 |
5 | **Before reading the code, see [docs/info.md](docs/info.md).**
6 |
7 | ## Clean and easy to use commands system
8 | Example code:
9 | 
10 | 
11 | 
12 |
13 | List of notable features:
14 | * Low-boilerplate argument handling system that supports arguments of various types, and optional arguments. Automatically generates an error if one of the args is invalid (eg, specifying a team that does not exist, or an ambiguous player name).
15 | * Intellisense for the arguments (The IDE will see `args: ["team:team?"]` and correctly type `args.team` as `Team | null`)
16 | * Callback-based menu system with builtin permission safety
17 | * Command handlers are provided with the command's usage stats (how long ago the command was used, etc)
18 | * Tap handling system
19 | * Permission handling system
20 | * Easy failing with fail() and its associated pattern
21 | * Automatically allows using a menu to resolve arguments left blank
22 |
23 | Interested in contributing? See [CONTRIBUTING.md](CONTRIBUTING.md), and thanks in advance!
24 |
25 | Contributors:
26 |
27 | [
](https://github.com/Brandons404/)
28 | [
](https://github.com/BalaM314/)
29 | [
](https://github.com/TheEt1234/)
30 | [
](https://github.com/buthed010203/)
31 |
32 | [
](https://github.com/Jurorno9/)
33 | [
](https://github.com/Dart25/)
34 | [
](https://github.com/kenos1/)
35 | [
](https://github.com/omnerom/)
36 |
37 | [
](https://github.com/Darthscion55/)
38 | [
](https://github.com/spentcud/)
39 |
40 | Join our Discord: [https://discord.gg/VpzcYSQ33Y](https://discord.gg/VpzcYSQ33Y)
41 |
42 |
--------------------------------------------------------------------------------
/archived/map-vote.js:
--------------------------------------------------------------------------------
1 | const utils = require('helper');
2 | importPackage(Packages.arc);
3 |
4 | let votes = {};
5 | let voteOngoing = false;
6 | let alreadyVoted = [];
7 | let voteStartTime = 0;
8 | const voteTime = 1.5 * 60000; // 1.5 mins
9 | let voteChangesMap = false;
10 | let serverCommands;
11 |
12 | const resetVotes = () => {
13 | alreadyVoted = [];
14 | voteStartTime = 0;
15 | voteOngoing = false;
16 | const maps = Vars.maps.customMaps().toArray();
17 | votes = {};
18 | for (let i = 0; i < maps.length; i++) {
19 | votes[i] = {
20 | name: maps[i].name(),
21 | total: 0,
22 | };
23 | }
24 | };
25 |
26 | const startVoteTimer = () => {
27 | voteOngoing = true;
28 | voteStartTime = Date.now();
29 | Timer.schedule(() => {
30 | if (!voteOngoing) return;
31 | let highestVotedMap = [];
32 | let highestVotes = 0;
33 |
34 | const maps = Object.keys(votes);
35 | maps.forEach((map) => {
36 | const vote = votes[map];
37 | if (vote.total > highestVotes) {
38 | highestVotes = vote.total;
39 | highestVotedMap = [map];
40 | return;
41 | }
42 |
43 | if (vote.total === highestVotes) {
44 | highestVotedMap.push(map);
45 | return;
46 | }
47 | });
48 |
49 | if (highestVotedMap.length > 1) {
50 | const votedMapNames = [];
51 | maps.forEach((num) => {
52 | if (votes[num].total > 0) {
53 | votedMapNames.push('[cyan]' + votes[num].name + '[yellow]: ' + votes[num].total);
54 | }
55 | });
56 |
57 | const winner = votes[highestVotedMap[Math.floor(Math.random() * highestVotedMap.length)]];
58 |
59 | Call.sendMessage(
60 | '[green]There was a tie between the following maps: \n' +
61 | '[yellow]' +
62 | votedMapNames.join('[green],\n[yellow]') +
63 | '\n[green]Picking random winner: [yellow]' +
64 | winner.name
65 | );
66 | serverCommands.handleMessage('nextmap ' + winner.name.split(' ').join('_'));
67 | resetVotes();
68 | if (voteChangesMap) {
69 | Call.sendMessage('[green]Changing map.');
70 | Events.fire(new GameOverEvent(Team.crux));
71 | }
72 | return;
73 | }
74 |
75 | Call.sendMessage(
76 | '[green]Map voting complete! The next map will be [yellow]' +
77 | votes[highestVotedMap[0]].name +
78 | ' [green]with [yellow]' +
79 | votes[highestVotedMap[0]].total +
80 | '[green] votes.'
81 | );
82 | serverCommands.handleMessage('nextmap ' + votes[highestVotedMap[0]].name.split(' ').join('_'));
83 | resetVotes();
84 | if (voteChangesMap) {
85 | Call.sendMessage('[green]Changing map.');
86 | Events.fire(new GameOverEvent(Team.crux));
87 | }
88 | return;
89 | }, voteTime / 1000);
90 | };
91 |
92 | const showVotes = () => {
93 | const totals = ['[green]Current votes: \n------------------------------'];
94 | const maps = Vars.maps.customMaps().toArray();
95 | for (let i = 0; i < maps.length; i++) {
96 | if (votes[i].total > 0) {
97 | totals.push('[cyan]' + maps[i].name() + '[yellow]: ' + votes[i].total);
98 | }
99 | }
100 | totals.push('[green]_________________________');
101 | Call.sendMessage(totals.join('\n'));
102 | };
103 |
104 | const getTimeLeft = () => {
105 | const endTime = voteStartTime + voteTime;
106 | const now = Date.now();
107 | const timeLeft = endTime - now;
108 |
109 | const minutes = Math.floor(timeLeft / 60000);
110 | const seconds = Math.floor((timeLeft % 60000) / 1000);
111 | return seconds == 60 ? minutes + 1 + ':00' : minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
112 | };
113 |
114 | Events.on(GameOverEvent, (e) => {
115 | resetVotes();
116 | });
117 |
118 | Events.on(ServerLoadEvent, (e) => {
119 | const clientCommands = Vars.netServer.clientCommands;
120 | serverCommands = Core.app.listeners.find(
121 | (l) => l instanceof Packages.mindustry.server.ServerControl
122 | ).handler;
123 | const runner = (method) => new Packages.arc.util.CommandHandler.CommandRunner({ accept: method });
124 |
125 | const voteChangesMapSaved = Core.settings.get('vnm', '');
126 |
127 | if (voteChangesMapSaved !== '') {
128 | voteChangesMap = voteChangesMapSaved === 'true' ? true : false;
129 | }
130 |
131 | resetVotes();
132 |
133 | // maps
134 | clientCommands.register(
135 | 'maps',
136 | 'list maps on server',
137 | runner((args, realP) => {
138 | realP.sendMessage(
139 | '\n[yellow]Use [white]/nextmap [lightgray]