├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build-app.js ├── build ├── img │ ├── api.png │ ├── arrow.svg │ ├── boss_rush.png │ ├── browse.svg │ ├── cancel.svg │ ├── cancel_yellow.svg │ ├── connect.svg │ ├── courses │ │ ├── 00-castle.png │ │ ├── 01-bob.png │ │ ├── 02-wf.png │ │ ├── 03-jrb.png │ │ ├── 04-ccm.png │ │ ├── 05-bbh.png │ │ ├── 06-hmc.png │ │ ├── 07-lll.png │ │ ├── 08-ssl.png │ │ ├── 09-ddd.png │ │ ├── 10-sl.png │ │ ├── 11-wdw.png │ │ ├── 12-ttm.png │ │ ├── 13-thi.png │ │ ├── 14-ttc.png │ │ ├── 15-rr.png │ │ ├── 16-bitdw.png │ │ ├── 17-bitfs.png │ │ ├── 18-bits.png │ │ ├── 19-pss.png │ │ ├── 20-metal.png │ │ ├── 21-wing.png │ │ ├── 22-vanish.png │ │ ├── 23-otr.png │ │ └── 24-aqua.png │ ├── delete.png │ ├── disconnect.svg │ ├── discord.svg │ ├── goomba.png │ ├── help.png │ ├── home.svg │ ├── host.svg │ ├── icon.ico │ ├── interactionless.svg │ ├── kirby.png │ ├── knuckles.png │ ├── lock.svg │ ├── luigi.png │ ├── mario.png │ ├── menu.png │ ├── menu.svg │ ├── menu_yellow.svg │ ├── n64.svg │ ├── net64.svg │ ├── pc.svg │ ├── peach.png │ ├── pj64_help1.png │ ├── pj64_help2.png │ ├── prop_hunt.svg │ ├── reddit.svg │ ├── regular.svg │ ├── rosalina.png │ ├── server.svg │ ├── settings.svg │ ├── shooter.svg │ ├── sonic.png │ ├── submit.png │ ├── tag.svg │ ├── toad.png │ ├── waluigi.png │ ├── wario.png │ ├── wario_ware.png │ ├── warning.svg │ └── yoshi.png ├── patches │ ├── 253710 │ ├── 277184 │ ├── 277308 │ ├── 277520 │ ├── 279854 │ ├── 800000 │ ├── 840000 │ ├── 862000 │ ├── 884000 │ ├── 909000 │ ├── 2773f8 │ ├── 27761c │ ├── 27770c │ ├── 2777c4 │ ├── 2794b0 │ ├── 29d35c │ ├── 37a550 │ ├── 37eb80 │ ├── 8a4000 │ ├── 8c4000 │ ├── 8e4000 │ ├── 9b0000 │ ├── 9b4000 │ ├── 9b4200 │ ├── 9ce000 │ ├── 9e5600 │ ├── ff9400 │ ├── ff9800 │ ├── ff9d00 │ ├── ff9d80 │ ├── ff9e80 │ ├── ff9f00 │ ├── ff9f40 │ ├── ff9f80 │ ├── ffa000 │ └── fff000 ├── processlist.node ├── styles │ ├── smme.ttf │ └── smme.woff └── winprocess.node ├── compat-list.js ├── jest-browser-setup.js ├── mocks └── WinProcess.ts ├── package.json ├── proto ├── ClientServerMessage.d.ts ├── ClientServerMessage.js ├── ClientServerMessage.proto ├── NETWORKING.md ├── ServerClientMessage.d.ts ├── ServerClientMessage.js ├── ServerClientMessage.proto ├── client-server │ ├── Authenticate.proto │ ├── ClientHandshake.proto │ └── ClientServer.proto ├── server-client │ ├── ConnectionDenied.proto │ ├── PlayerUpdate.proto │ ├── ServerClient.proto │ ├── ServerHandshake.proto │ └── ServerMessage.proto └── shared │ ├── Chat.proto │ ├── Compression.proto │ ├── GameMode.proto │ ├── MetaData.proto │ ├── Ping.proto │ ├── Player.proto │ └── PlayerData.proto ├── src ├── asm │ ├── 909000.txt │ ├── characterconverter.txt │ ├── ff9800.txt │ ├── ff9d00.txt │ ├── ff9d80.txt │ └── ffa000.txt ├── declarations │ ├── discord-rich-presence.d.ts │ └── winprocess.ts ├── main │ ├── Connection.ts │ ├── Connector.ts │ ├── Emulator.ts │ ├── HotkeyManager.ts │ └── index.ts ├── models │ ├── Emulator.mock.ts │ ├── Emulator.model.ts │ ├── Message.model.ts │ ├── Release.model.ts │ ├── Server.model.ts │ └── State.model.ts ├── renderer │ ├── Connector.ts │ ├── GamepadManager.ts │ ├── Request.ts │ ├── actions │ │ ├── chat.ts │ │ ├── connection.ts │ │ ├── emulator.ts │ │ ├── models │ │ │ ├── chat.model.ts │ │ │ ├── connection.model.ts │ │ │ ├── emulator.model.ts │ │ │ ├── router.model.ts │ │ │ ├── save.model.ts │ │ │ ├── server.model.ts │ │ │ └── snackbar.model.ts │ │ ├── save.ts │ │ ├── server.ts │ │ └── snackbar.ts │ ├── components │ │ ├── areas │ │ │ ├── ChatArea.tsx │ │ │ ├── ConnectArea.tsx │ │ │ ├── ConnectionArea.scss │ │ │ ├── ConnectionArea.tsx │ │ │ ├── HostArea.scss │ │ │ ├── HostArea.tsx │ │ │ ├── NavigationArea.scss │ │ │ ├── NavigationArea.tsx │ │ │ ├── SendPasswordArea.scss │ │ │ ├── SendPasswordArea.tsx │ │ │ ├── ServerArea.scss │ │ │ ├── ServerArea.tsx │ │ │ ├── ServerHostArea.scss │ │ │ ├── ServerHostArea.tsx │ │ │ ├── TopBarArea.scss │ │ │ └── TopBarArea.tsx │ │ ├── buttons │ │ │ ├── HotkeyButton.scss │ │ │ ├── HotkeyButton.tsx │ │ │ ├── NavigationButton.scss │ │ │ ├── NavigationButton.tsx │ │ │ ├── SMMButton.scss │ │ │ ├── SMMButton.tsx │ │ │ └── ToggleButton.tsx │ │ ├── dialogs │ │ │ ├── NewVersionDialog.scss │ │ │ └── NewVersionDialog.tsx │ │ ├── forms │ │ │ ├── HostForm.scss │ │ │ └── HostForm.tsx │ │ ├── headers │ │ │ ├── HostHeader.scss │ │ │ └── HostHeader.tsx │ │ ├── helpers │ │ │ ├── ExternalLink.tsx │ │ │ ├── ProgressSpinner.scss │ │ │ └── ProgressSpinner.tsx │ │ ├── panels │ │ │ ├── ChatMessagePanel.tsx │ │ │ ├── ConsolePanel.scss │ │ │ ├── ConsolePanel.tsx │ │ │ ├── RadarPanel.scss │ │ │ ├── RadarPanel.tsx │ │ │ ├── ServerPanel.scss │ │ │ ├── ServerPanel.tsx │ │ │ ├── SnackbarPanel.scss │ │ │ ├── SnackbarPanel.tsx │ │ │ ├── WarningPanel.scss │ │ │ └── WarningPanel.tsx │ │ └── views │ │ │ ├── AboutView.tsx │ │ │ ├── AppView.tsx │ │ │ ├── BrowseView.scss │ │ │ ├── BrowseView.tsx │ │ │ ├── ConnectView.scss │ │ │ ├── ConnectView.tsx │ │ │ ├── EmulatorView.tsx │ │ │ ├── FaqView.tsx │ │ │ ├── HostView.scss │ │ │ ├── HostView.tsx │ │ │ ├── MainView.tsx │ │ │ ├── SettingsView.scss │ │ │ └── SettingsView.tsx │ ├── index.tsx │ ├── middlewares │ │ ├── server-middleware.ts │ │ └── snackbar-middleware.ts │ ├── reducers │ │ ├── chat.ts │ │ ├── connection.ts │ │ ├── emulator.ts │ │ ├── index.ts │ │ ├── router.ts │ │ ├── save.ts │ │ ├── server.ts │ │ └── snackbar.ts │ ├── template.html │ └── utils │ │ ├── chat.util.ts │ │ └── helper.util.ts ├── styles │ └── global.scss └── utils │ └── Buffer.util.ts ├── tsconfig.json ├── webpack.config.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "targets": { 5 | "electron": "2.0.17" 6 | }, 7 | "corejs": "3.6.4", 8 | "useBuiltIns": "usage" 9 | }], 10 | ["@babel/preset-typescript", { 11 | "isTSX": true, 12 | "allExtensions": true 13 | }], 14 | "@babel/react" 15 | ], 16 | "plugins": [ 17 | "@babel/syntax-dynamic-import", 18 | "@babel/proposal-class-properties", 19 | "@babel/proposal-object-rest-spread" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | proto/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react/recommended", 8 | "standard-with-typescript" 9 | ], 10 | "plugins": [ 11 | "@typescript-eslint", 12 | "react", 13 | "jsdoc" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true, 20 | "modules": true 21 | }, 22 | "project": "./tsconfig.json" 23 | }, 24 | "rules": { 25 | "@typescript-eslint/explicit-function-return-type": 0, 26 | "@typescript-eslint/default-param-last": 0, 27 | "@typescript-eslint/member-delimiter-style": [2, { 28 | "multiline": { 29 | "delimiter": "none", 30 | "requireLast": false 31 | }, 32 | "singleline": { 33 | "delimiter": "comma", 34 | "requireLast": false 35 | }, 36 | "overrides": { 37 | "typeLiteral": { 38 | "multiline": { 39 | "delimiter": "comma", 40 | "requireLast": true 41 | } 42 | } 43 | } 44 | }], 45 | "@typescript-eslint/no-empty-function": 1, 46 | "@typescript-eslint/no-floating-promises": 1, 47 | "@typescript-eslint/no-misused-promises": 1, 48 | "@typescript-eslint/no-non-null-assertion": 1, 49 | "@typescript-eslint/prefer-nullish-coalescing": 1, 50 | "@typescript-eslint/promise-function-async": 0, 51 | "@typescript-eslint/restrict-plus-operands": 1, 52 | "@typescript-eslint/restrict-template-expressions": 0, 53 | "@typescript-eslint/strict-boolean-expressions": 0, 54 | "max-len": [2, { "code": 120 }], 55 | "no-case-declarations": 0, 56 | "no-empty": [2, { "allowEmptyCatch": true }], 57 | "no-unused-vars": 1, 58 | "react/prop-types": 0, 59 | "react/jsx-no-target-blank": 0, 60 | "jsdoc/check-param-names": 1, 61 | "jsdoc/check-tag-names": 1, 62 | "jsdoc/check-types": 1, 63 | "jsdoc/newline-after-description": 1, 64 | "jsdoc/require-hyphen-before-param-description": 1, 65 | "jsdoc/require-param": 1, 66 | "jsdoc/require-param-description": 1, 67 | "jsdoc/require-param-name": 1, 68 | "jsdoc/require-param-type": 1, 69 | "jsdoc/require-returns-description": 1, 70 | "jsdoc/require-returns-type": 1 71 | }, 72 | "env": { 73 | "browser": true, 74 | "worker": true, 75 | "jest": true 76 | }, 77 | "settings": { 78 | "react": { 79 | "version": "detect" 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .credentials 2 | /.idea/ 3 | /node_modules/ 4 | /build/* 5 | !/build/patches/ 6 | !/build/img/ 7 | !/build/styles/ 8 | /build/styles/renderer.* 9 | !/build/winprocess.node 10 | !/build/processlist.node 11 | /release/ 12 | yarn-error.log 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12.14.1 4 | 5 | env: 6 | global: 7 | - CXX=g++-4.8 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | 16 | jobs: 17 | include: 18 | - stage: Lint 19 | script: yarn lint 20 | name: "Lint" 21 | # - script: yarn test 22 | # name: "Unit Tests" 23 | - stage: Build 24 | script: yarn build 25 | name: "Build" 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 12 | }, 13 | "program": "${workspaceRoot}/build/index.js", 14 | "protocol": "inspector" 15 | }, 16 | { 17 | "name": "Debug Renderer Process", 18 | "type": "chrome", 19 | "request": "launch", 20 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 21 | "windows": { 22 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 23 | }, 24 | "runtimeArgs": [ 25 | "http://localhost:4000", 26 | "--remote-debugging-port=9222" 27 | ], 28 | "sourceMaps": true, 29 | "webRoot": "${workspaceRoot}" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "typescript.tsdk": "node_modules/typescript/lib" 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | First off, thanks for taking the time to contribute! 2 | 3 | # How to contribute to Net64+ 4 | 5 | Here are some rules and guidelines for contributing. 6 | 7 | ## Make an issue 8 | 9 | You should always use GitHub issues to talk about things that you want to be implemented or bugs that you found. 10 | If you are just writing in some chat app like Discord, it is very likely that noone who is responsible for the code will ever see it, so please use GitHub as a discussion platform. 11 | 12 | #### Did you find a bug? 13 | 14 | - Check our issue section, if there is already an opened issue and give your feedback 15 | - If there is not yet an issue, please don't hesitate opening a new one 16 | 17 | #### Do you want to have a new feature? 18 | 19 | Basically the same 20 | 21 | #### Do you want to make your own code changes? 22 | 23 | Before you start coding with the hope that your code will be merged, you should start a discussion about what you want to implement. Maybe we don't want or need it for some reason and we don't want you to work for nothing. 24 | 25 | ## Commit messages 26 | 27 | - Try to describe your changes as closely as possible 28 | - If there is an issue related to your changes, name it in your commit header 29 | 30 | ## Clean Code 31 | 32 | We encourage you to read ![JavaScript Clean Code Principles](https://github.com/ryanmcdermott/clean-code-javascript) and use it as a guideline while coding. 33 | 34 | ## Styleguide 35 | 36 | We use ![JavaScript Standard Style](standardjs.com). 37 | 38 | ESLint is set up to help you out on all linting rules. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Mario Reder 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net64+ Client 2 | 3 | ![GitHub All Releases](https://img.shields.io/github/downloads/Tarnadas/net64plus/total) 4 | ![GitHub Releases](https://img.shields.io/github/downloads/Tarnadas/net64plus/latest/total) 5 | [![LGTM Grade](https://img.shields.io/lgtm/grade/javascript/github/Tarnadas/net64plus)](https://lgtm.com/projects/g/Tarnadas/net64plus) 6 | [![Discord](https://discordapp.com/api/guilds/559982917049253898/widget.png)](https://discord.gg/GgGUKH8) 7 | [![Build Status](https://api.travis-ci.org/Tarnadas/net64plus.svg?branch=master)](https://travis-ci.org/Tarnadas/net64plus) 8 | 9 | Net64 aka SM64O allows playing Super Mario 64 in an online multiplayer mode. 10 | Net64+ is the official continuation of the program and features an integrated server list. 11 | 12 | This repository includes the client software. If you want to host your own dedicated server, please visit the [server repository](https://github.com/Tarnadas/net64plus-server). 13 | 14 | ## Download 15 | 16 | Download the client/server/emulator bundle in the [release section](https://github.com/Tarnadas/net64plus/releases). 17 | 18 | ## Server List 19 | 20 | There is a [public server list](https://net64-mod.github.io/servers) of all Net64+ server, that have enabled listing. 21 | -------------------------------------------------------------------------------- /build-app.js: -------------------------------------------------------------------------------- 1 | const packager = require('electron-packager') 2 | const rimraf = require('rimraf') 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const packageJson = JSON.parse(fs.readFileSync('./package.json')) 7 | const out = path.normalize(`./release/${packageJson.version}`) 8 | if (!fs.existsSync('./release')) { 9 | fs.mkdirSync('./release') 10 | } 11 | if (!fs.existsSync(out)) { 12 | fs.mkdirSync(out) 13 | } 14 | 15 | packager({ 16 | name: 'Net64+', 17 | dir: './build', 18 | out, 19 | arch: 'x64', 20 | platform: 'win32', 21 | appVersion: packageJson.version, 22 | icon: './build/img/icon.ico', 23 | overwrite: true 24 | }, (err, appPaths) => { 25 | if (err) throw err 26 | fs.writeFileSync(path.join(appPaths[0], 'resources/app/package.json'), JSON.stringify(packageJson)) 27 | fs.mkdirSync(path.join(appPaths[0], `patches`)) 28 | fs.readdirSync('./build/patches').map(val => `./build/patches/${val}`).forEach(file => { 29 | fs.createReadStream(file).pipe(fs.createWriteStream(path.join(appPaths[0], `patches/${file.split('patches/')[1]}`))) 30 | }) 31 | rimraf(path.join(appPaths[0], 'resources/app/patches'), () => {}) 32 | }) 33 | -------------------------------------------------------------------------------- /build/img/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/api.png -------------------------------------------------------------------------------- /build/img/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/boss_rush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/boss_rush.png -------------------------------------------------------------------------------- /build/img/browse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/cancel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/cancel_yellow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/connect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/courses/00-castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/00-castle.png -------------------------------------------------------------------------------- /build/img/courses/01-bob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/01-bob.png -------------------------------------------------------------------------------- /build/img/courses/02-wf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/02-wf.png -------------------------------------------------------------------------------- /build/img/courses/03-jrb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/03-jrb.png -------------------------------------------------------------------------------- /build/img/courses/04-ccm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/04-ccm.png -------------------------------------------------------------------------------- /build/img/courses/05-bbh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/05-bbh.png -------------------------------------------------------------------------------- /build/img/courses/06-hmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/06-hmc.png -------------------------------------------------------------------------------- /build/img/courses/07-lll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/07-lll.png -------------------------------------------------------------------------------- /build/img/courses/08-ssl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/08-ssl.png -------------------------------------------------------------------------------- /build/img/courses/09-ddd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/09-ddd.png -------------------------------------------------------------------------------- /build/img/courses/10-sl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/10-sl.png -------------------------------------------------------------------------------- /build/img/courses/11-wdw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/11-wdw.png -------------------------------------------------------------------------------- /build/img/courses/12-ttm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/12-ttm.png -------------------------------------------------------------------------------- /build/img/courses/13-thi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/13-thi.png -------------------------------------------------------------------------------- /build/img/courses/14-ttc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/14-ttc.png -------------------------------------------------------------------------------- /build/img/courses/15-rr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/15-rr.png -------------------------------------------------------------------------------- /build/img/courses/16-bitdw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/16-bitdw.png -------------------------------------------------------------------------------- /build/img/courses/17-bitfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/17-bitfs.png -------------------------------------------------------------------------------- /build/img/courses/18-bits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/18-bits.png -------------------------------------------------------------------------------- /build/img/courses/19-pss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/19-pss.png -------------------------------------------------------------------------------- /build/img/courses/20-metal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/20-metal.png -------------------------------------------------------------------------------- /build/img/courses/21-wing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/21-wing.png -------------------------------------------------------------------------------- /build/img/courses/22-vanish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/22-vanish.png -------------------------------------------------------------------------------- /build/img/courses/23-otr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/23-otr.png -------------------------------------------------------------------------------- /build/img/courses/24-aqua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/courses/24-aqua.png -------------------------------------------------------------------------------- /build/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/delete.png -------------------------------------------------------------------------------- /build/img/disconnect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build/img/goomba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/goomba.png -------------------------------------------------------------------------------- /build/img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/help.png -------------------------------------------------------------------------------- /build/img/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/host.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/icon.ico -------------------------------------------------------------------------------- /build/img/interactionless.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/kirby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/kirby.png -------------------------------------------------------------------------------- /build/img/knuckles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/knuckles.png -------------------------------------------------------------------------------- /build/img/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/luigi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/luigi.png -------------------------------------------------------------------------------- /build/img/mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/mario.png -------------------------------------------------------------------------------- /build/img/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/menu.png -------------------------------------------------------------------------------- /build/img/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/menu_yellow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/n64.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/pc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/peach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/peach.png -------------------------------------------------------------------------------- /build/img/pj64_help1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/pj64_help1.png -------------------------------------------------------------------------------- /build/img/pj64_help2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/pj64_help2.png -------------------------------------------------------------------------------- /build/img/reddit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/rosalina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/rosalina.png -------------------------------------------------------------------------------- /build/img/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/shooter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/sonic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/sonic.png -------------------------------------------------------------------------------- /build/img/submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/submit.png -------------------------------------------------------------------------------- /build/img/toad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/toad.png -------------------------------------------------------------------------------- /build/img/waluigi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/waluigi.png -------------------------------------------------------------------------------- /build/img/wario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/wario.png -------------------------------------------------------------------------------- /build/img/wario_ware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/wario_ware.png -------------------------------------------------------------------------------- /build/img/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/img/yoshi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/img/yoshi.png -------------------------------------------------------------------------------- /build/patches/253710: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/253710 -------------------------------------------------------------------------------- /build/patches/277184: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /build/patches/277308: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /build/patches/2773f8: -------------------------------------------------------------------------------- 1 | 6 -------------------------------------------------------------------------------- /build/patches/277520: -------------------------------------------------------------------------------- 1 | $ -------------------------------------------------------------------------------- /build/patches/27761c: -------------------------------------------------------------------------------- 1 | ( -------------------------------------------------------------------------------- /build/patches/27770c: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /build/patches/2777c4: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /build/patches/2794b0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/patches/279854: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/patches/29d35c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/29d35c -------------------------------------------------------------------------------- /build/patches/37a550: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/37a550 -------------------------------------------------------------------------------- /build/patches/37eb80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/37eb80 -------------------------------------------------------------------------------- /build/patches/800000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/800000 -------------------------------------------------------------------------------- /build/patches/840000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/840000 -------------------------------------------------------------------------------- /build/patches/862000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/862000 -------------------------------------------------------------------------------- /build/patches/884000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/884000 -------------------------------------------------------------------------------- /build/patches/8a4000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/8a4000 -------------------------------------------------------------------------------- /build/patches/8c4000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/8c4000 -------------------------------------------------------------------------------- /build/patches/8e4000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/8e4000 -------------------------------------------------------------------------------- /build/patches/909000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/909000 -------------------------------------------------------------------------------- /build/patches/9b0000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/9b0000 -------------------------------------------------------------------------------- /build/patches/9b4000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/9b4000 -------------------------------------------------------------------------------- /build/patches/9b4200: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/9b4200 -------------------------------------------------------------------------------- /build/patches/9ce000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/9ce000 -------------------------------------------------------------------------------- /build/patches/9e5600: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/9e5600 -------------------------------------------------------------------------------- /build/patches/ff9400: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9400 -------------------------------------------------------------------------------- /build/patches/ff9800: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9800 -------------------------------------------------------------------------------- /build/patches/ff9d00: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9d00 -------------------------------------------------------------------------------- /build/patches/ff9d80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9d80 -------------------------------------------------------------------------------- /build/patches/ff9e80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9e80 -------------------------------------------------------------------------------- /build/patches/ff9f00: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9f00 -------------------------------------------------------------------------------- /build/patches/ff9f40: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9f40 -------------------------------------------------------------------------------- /build/patches/ff9f80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ff9f80 -------------------------------------------------------------------------------- /build/patches/ffa000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/ffa000 -------------------------------------------------------------------------------- /build/patches/fff000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/patches/fff000 -------------------------------------------------------------------------------- /build/processlist.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/processlist.node -------------------------------------------------------------------------------- /build/styles/smme.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/styles/smme.ttf -------------------------------------------------------------------------------- /build/styles/smme.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/styles/smme.woff -------------------------------------------------------------------------------- /build/winprocess.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/build/winprocess.node -------------------------------------------------------------------------------- /compat-list.js: -------------------------------------------------------------------------------- 1 | module.exports = function getCompatMin (currentVersion) { 2 | const [ major, minor ] = currentVersion.split('.').map(n => Number(n)) 3 | if (major === 2 && minor === 1) return [ '2', '0' ] 4 | if (major === 2 && minor === 2) return [ '2', '0' ] 5 | if (major === 2 && minor === 3) return [ '2', '0' ] 6 | if (major === 2 && minor === 4) return [ '2', '0' ] 7 | if (major === 2 && minor === 5) return [ '2', '0' ] 8 | throw new Error(`Compatibility list found unknown version ${currentVersion}`) 9 | } 10 | -------------------------------------------------------------------------------- /jest-browser-setup.js: -------------------------------------------------------------------------------- 1 | const encoding = require('text-encoding') 2 | 3 | global.TextDecoder = encoding.TextDecoder 4 | global.TextEncoder = encoding.TextEncoder 5 | -------------------------------------------------------------------------------- /mocks/WinProcess.ts: -------------------------------------------------------------------------------- 1 | export default jest.mock('winprocess', () => ({ 2 | Process: (processId: number) => ({ 3 | open: () => {}, 4 | readMemory: (offset: number, length: number) => { 5 | return Buffer.alloc(length) 6 | }, 7 | writeMemory: (offset: number, buffer: Buffer) => {} 8 | }) 9 | })) 10 | -------------------------------------------------------------------------------- /proto/ClientServerMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "client-server/ClientServer.proto"; 4 | import "shared/Compression.proto"; 5 | 6 | message ClientServerMessage { 7 | Compression compression = 1; 8 | uint32 uncompressed_size = 2; 9 | oneof message { 10 | bytes compressed_data = 3; 11 | ClientServer data = 4; 12 | } 13 | } -------------------------------------------------------------------------------- /proto/NETWORKING.md: -------------------------------------------------------------------------------- 1 | # Net64+ Networking Protocol 2 | 3 | Net64+ >=2.0 will entirely use [Protocol Buffer](https://developers.google.com/protocol-buffers/docs/overview) for message serialization. 4 | 5 | The type of compression that will be used is still WIP. 6 | Candidates are [ZSTD](http://facebook.github.io/zstd) or GZIP. ZSTD has the advantage, that it is heavily optimized for small data compression. You need to feed it a lot of training data and it will generate an optimized dictionary, which can then be used for future compressions. 7 | 8 | ## Server to Client 9 | 10 | A server-client-message has the following definition: 11 | 12 | ```proto 13 | message ServerClientMessage { 14 | enum Compression { 15 | NONE = 0; 16 | ZSTD = 1; 17 | GZIP = 2; 18 | } 19 | Compression compression = 1; 20 | oneof message { 21 | bytes compressed_data = 2; 22 | ServerClient data = 3; 23 | } 24 | } 25 | ``` 26 | 27 | Any message received from a client can potentially be compressed. The compression type can be seen in field#1. The `ServerClient` type is the uncompressed message. If any compression method was used, the compressed data can instead be decompressed and deserialized to a `ServerClient` object. 28 | 29 | #### ServerClient Object 30 | 31 | A `ServerClient` object has the following definition: 32 | 33 | ```proto 34 | message ServerClient { 35 | enum MessageType { 36 | HANDSHAKE = 0; 37 | PING = 1; 38 | SERVER_MESSAGE = 2; 39 | PLAYER_LIST_UPDATE = 3; 40 | PLAYER_UPDATE = 4; 41 | PLAYER_DATA = 128; 42 | META_DATA = 129; 43 | META_MESSAGE = 130; 44 | CHAT_MESSAGE = 131; 45 | } 46 | MessageType message_type = 1; 47 | oneof message { 48 | Handshake handshake = 2; 49 | Ping ping = 3; 50 | ServerMessage server_message = 3; 51 | PlayerListUpdate player_list_update_message = 4; 52 | PlayerUpdate player_update_message = 5; 53 | MetaData meta_data_message = 6; 54 | Chat chat_message = 7; 55 | } 56 | } 57 | ``` 58 | 59 | The most significant bit of the `MessageType` enum reflects whether the message contains data that is used by the emulator. 60 | 61 | A `ServerMessage` contains data which is used by the client program to determine various client-specific tasks. 62 | 63 | A `PlayerListUpdate` contains a list of all currently connected players. 64 | 65 | A `PlayerUpdate` is a single player update, because it is more lightweight to only send the diff, if a player object changes. 66 | 67 | `PlayerData` contains an array of player data from all currently connected players, which will be sent with ~30fps. 68 | 69 | `MetaData` contains any additional data, that needs to be sent from the Emulator. 70 | 71 | `Chat` is a chat message. 72 | ![You don't day](https://i.imgur.com/38BlfVp.png?1) -------------------------------------------------------------------------------- /proto/ServerClientMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "server-client/ServerClient.proto"; 4 | import "shared/Compression.proto"; 5 | 6 | message ServerClientMessage { 7 | Compression compression = 1; 8 | uint32 uncompressed_size = 2; 9 | oneof message { 10 | bytes compressed_data = 3; 11 | ServerClient data = 4; 12 | } 13 | } -------------------------------------------------------------------------------- /proto/client-server/Authenticate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Authenticate { 4 | string password = 1; 5 | } -------------------------------------------------------------------------------- /proto/client-server/ClientHandshake.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message ClientHandshake { 4 | uint32 major = 1; 5 | uint32 minor = 2; 6 | uint32 character_id = 3; 7 | string username = 4; 8 | } -------------------------------------------------------------------------------- /proto/client-server/ClientServer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "client-server/ClientHandshake.proto"; 4 | import "shared/Ping.proto"; 5 | import "shared/Player.proto"; 6 | import "client-server/Authenticate.proto"; 7 | import "shared/PlayerData.proto"; 8 | import "shared/MetaData.proto"; 9 | import "shared/Chat.proto"; 10 | 11 | message ClientServer { 12 | enum MessageType { 13 | UNKNOWN = 0; 14 | HANDSHAKE = 2; 15 | PING = 3; 16 | PLAYER_UPDATE = 6; 17 | AUTHENTICATE = 7; 18 | PLAYER_DATA = 128; 19 | META_DATA = 129; 20 | CHAT = 130; 21 | } 22 | MessageType message_type = 1; 23 | oneof message { 24 | ClientHandshake handshake = 2; 25 | Ping ping = 3; 26 | Player player = 6; 27 | Authenticate authenticate = 7; 28 | PlayerData player_data = 128; 29 | MetaData meta_data = 129; 30 | Chat chat = 130; 31 | } 32 | } -------------------------------------------------------------------------------- /proto/server-client/ConnectionDenied.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message ConnectionDenied { 4 | enum Reason { 5 | SERVER_FULL = 0; 6 | WRONG_VERSION = 1; 7 | } 8 | Reason reason = 1; 9 | oneof message { 10 | ServerFull server_full = 2; 11 | WrongVersion wrong_version = 3; 12 | } 13 | } 14 | 15 | message ServerFull { 16 | uint32 max_players = 1; 17 | } 18 | 19 | message WrongVersion { 20 | uint32 major_version = 1; 21 | uint32 minor_version = 2; 22 | } -------------------------------------------------------------------------------- /proto/server-client/PlayerUpdate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "shared/Player.proto"; 4 | 5 | message PlayerUpdate { 6 | uint32 player_id = 1; 7 | Player player = 2; 8 | } 9 | 10 | message PlayerListUpdate { 11 | repeated PlayerUpdate player_updates = 1; 12 | } -------------------------------------------------------------------------------- /proto/server-client/ServerClient.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "server-client/ServerHandshake.proto"; 4 | import "shared/Ping.proto"; 5 | import "server-client/ServerMessage.proto"; 6 | import "server-client/PlayerUpdate.proto"; 7 | import "shared/PlayerData.proto"; 8 | import "shared/MetaData.proto"; 9 | import "shared/Chat.proto"; 10 | 11 | message ServerClient { 12 | enum MessageType { 13 | UNKNOWN = 0; 14 | HANDSHAKE = 2; 15 | PING = 3; 16 | SERVER_MESSAGE = 4; 17 | PLAYER_LIST_UPDATE = 5; 18 | PLAYER_UPDATE = 6; 19 | PLAYER_DATA = 128; 20 | META_DATA = 129; 21 | CHAT = 130; 22 | } 23 | MessageType message_type = 1; 24 | oneof message { 25 | ServerHandshake handshake = 2; 26 | Ping ping = 3; 27 | ServerMessage server_message = 4; 28 | PlayerListUpdate player_list_update = 5; 29 | PlayerUpdate player_update = 6; 30 | PlayerData player_data = 128; 31 | MetaData meta_data = 129; 32 | Chat chat = 130; 33 | } 34 | } -------------------------------------------------------------------------------- /proto/server-client/ServerHandshake.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "shared/GameMode.proto"; 4 | import "server-client/PlayerUpdate.proto"; 5 | 6 | message ServerHandshake { 7 | uint32 player_id = 1; 8 | string ip = 2; 9 | uint32 port = 3; 10 | string domain = 4; 11 | string name = 5; 12 | string description = 6; 13 | PlayerListUpdate player_list = 7; 14 | string country_code = 8; 15 | GameModeType game_mode = 9; 16 | bool password_required = 10; 17 | } -------------------------------------------------------------------------------- /proto/server-client/ServerMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "shared/GameMode.proto"; 4 | import "server-client/ConnectionDenied.proto"; 5 | 6 | message ServerMessage { 7 | enum MessageType { 8 | CONNECTION_DENIED = 0; 9 | GAME_MODE = 1; 10 | PLAYER_REORDER = 2; 11 | ERROR = 3; 12 | AUTHENTICATION = 4; 13 | } 14 | MessageType message_type = 1; 15 | oneof message { 16 | ConnectionDenied connection_denied = 2; 17 | GameMode game_mode = 3; 18 | PlayerReorder player_reorder = 4; 19 | Error error = 5; 20 | Authentication authentication = 6; 21 | } 22 | } 23 | 24 | message PlayerReorder { 25 | bool grant_token = 1; 26 | uint32 player_id = 2; 27 | } 28 | 29 | message Error { 30 | enum ErrorType { 31 | UNKNOWN = 0; 32 | BAD_REQUEST = 400; 33 | UNAUTHORIZED = 401; 34 | TOO_MANY_REQUESTS = 429; 35 | INTERNAL_SERVER_ERROR = 500; 36 | } 37 | ErrorType error_type = 1; 38 | string message = 2; 39 | } 40 | 41 | message Authentication { 42 | enum Status { 43 | ACCEPTED = 0; 44 | DENIED = 1; 45 | } 46 | Status status = 1; 47 | uint32 throttle = 2; 48 | } -------------------------------------------------------------------------------- /proto/shared/Chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Chat { 4 | enum ChatType { 5 | GLOBAL = 0; 6 | PRIVATE = 1; 7 | COMMAND = 255; 8 | } 9 | ChatType chat_type = 1; 10 | uint32 sender_id = 2; 11 | string message = 3; 12 | oneof message_type { 13 | ChatGlobal global = 4; 14 | ChatPrivate private = 5; 15 | ChatCommand command = 255; 16 | } 17 | } 18 | 19 | message ChatGlobal { 20 | } 21 | 22 | message ChatPrivate { 23 | uint32 receiver_id = 1; 24 | } 25 | 26 | message ChatCommand { 27 | repeated string arguments = 1; 28 | } -------------------------------------------------------------------------------- /proto/shared/Compression.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | enum Compression { 4 | NONE = 0; 5 | ZSTD = 1; 6 | GZIP = 2; 7 | } -------------------------------------------------------------------------------- /proto/shared/GameMode.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message GameMode { 4 | GameModeType game_mode = 1; 5 | } 6 | enum GameModeType { 7 | NONE = 0; 8 | DEFAULT = 1; 9 | THIRD_PERSON_SHOOTER = 2; 10 | INTERACTIONLESS = 3; 11 | PROP_HUNT = 4; 12 | BOSS_RUSH = 5; 13 | TAG = 6; 14 | WARIO_WARE = 8; 15 | } -------------------------------------------------------------------------------- /proto/shared/MetaData.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Meta { 4 | uint32 length = 1; 5 | uint32 address = 2; 6 | bytes data = 3; 7 | } 8 | 9 | message MetaData { 10 | repeated Meta meta_data = 1; 11 | } -------------------------------------------------------------------------------- /proto/shared/Ping.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Ping {} -------------------------------------------------------------------------------- /proto/shared/Player.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Player { 4 | string username = 1; 5 | uint32 character_id = 2; 6 | } -------------------------------------------------------------------------------- /proto/shared/PlayerData.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message PlayerData { 4 | uint32 data_length = 1; 5 | repeated PlayerBytes player_bytes = 2; 6 | } 7 | 8 | message PlayerBytes { 9 | uint32 player_id = 1; 10 | bytes player_data = 2; 11 | } -------------------------------------------------------------------------------- /src/asm/909000.txt: -------------------------------------------------------------------------------- 1 | .org 0x7f00 2 | !main: 3 | addiu sp, sp, $ffe4 4 | sw ra, $14 (SP) 5 | lui t0, $8039 6 | lw t2, $be28 (T0) 7 | beq t2, r0, !end 8 | nop 9 | lw t3, $0004 (T2) 10 | lui at, $8016 11 | ori at, at, $f5b0 12 | bne t3, at, !end 13 | nop 14 | lui t0, $80ff 15 | lbu t2, $5ff4 (T0) 16 | addiu at, r0, $8 17 | bne t2, at, !end 18 | nop 19 | ori t1, t0, $1b00 20 | or a3, r0, r0 21 | or t4, r0, r0 22 | !Loopcheck: 23 | lbu t3, $771a (T0) 24 | slt at, t3, a3 25 | bne at, r0, !endofloop 26 | nop 27 | lbu t4, $7700 (T0) 28 | or a3, t3, r0 29 | !endofloop: 30 | addiu t0, t0, $0100 31 | bne t0, t1, !Loopcheck 32 | nop 33 | or t5, a3, r0 34 | sw a3, $0018 (SP) 35 | or a3, t4, r0 36 | lui t0, $8091 37 | lbu t0, $02ff (T0) 38 | bne t5, t0, !noplayerwin 39 | nop 40 | addiu a0, r0, $0020 41 | addiu a1, r0, $00d1 42 | lui a2, $8091 43 | jal $2d62d8 44 | ori a2, a2, $02a0 45 | beq r0, r0, !reunition 46 | nop 47 | 48 | 49 | !noplayerwin: 50 | addiu a0, r0, $0020 51 | addiu a1, r0, $00d1 52 | lui a2, $8091 53 | jal $2d62d8 54 | ori a2, a2, $0260 55 | 56 | 57 | lw a3, $0018 (SP) 58 | addiu a0, r0, $0020 59 | addiu a1, r0, $00b9 60 | lui a2, $8091 61 | jal $2d62d8 62 | ori a2, a2, $0280 63 | !reunition: 64 | lui t0, $80ff 65 | lbu t1, $7714 (T0) 66 | lui at, $8033 67 | sh t1, $ddf8 (AT) 68 | 69 | lui t1, $8034 70 | sw r0, $afa0 (T1) 71 | jal $ffa000 72 | nop 73 | jal $910d00 74 | lui t0, $801a 75 | !end: 76 | lw ra, $14 (SP) 77 | jr ra 78 | addiu sp, sp, $1c 79 | 80 | .org 0x7d00 81 | !nops: 82 | sb r0, $7860 (T0) 83 | sb r0, $7870 (T0) 84 | sb r0, $7880 (T0) 85 | sb r0, $7890 (T0) 86 | sb r0, $78a0 (T0) 87 | sb r0, $78b0 (T0) 88 | sb r0, $78c0 (T0) 89 | sb r0, $78d0 (T0) 90 | sb r0, $78e0 (T0) 91 | sb r0, $78f0 (T0) 92 | sb r0, $7900 (T0) 93 | sb r0, $7910 (T0) 94 | sb r0, $7920 (T0) 95 | sb r0, $7930 (T0) 96 | sb r0, $7940 (T0) 97 | sb r0, $7950 (T0) 98 | sb r0, $7960 (T0) 99 | sb r0, $7970 (T0) 100 | sb r0, $7980 (T0) 101 | sb r0, $7990 (T0) 102 | sb r0, $79a0 (T0) 103 | sb r0, $79b0 (T0) 104 | sb r0, $79c0 (T0) 105 | sb r0, $79d0 (T0) 106 | sb r0, $79e0 (T0) 107 | sb r0, $79f0 (T0) 108 | sb r0, $7a00 (T0) 109 | sb r0, $7a10 (T0) 110 | sb r0, $7a20 (T0) 111 | sb r0, $7a30 (T0) 112 | sb r0, $7a40 (T0) 113 | sb r0, $7a50 (T0) 114 | sb r0, $7a60 (T0) 115 | sb r0, $7c40 (T0) 116 | jr ra 117 | sb r0, $7c50 (T0) -------------------------------------------------------------------------------- /src/asm/characterconverter.txt: -------------------------------------------------------------------------------- 1 | //yoshi is at 80570000 to 80590E00 model size 2ac0 2 | 3 | .org 0x861c0 4 | !hook: 5 | addiu sp, sp, $ffe8 6 | sw ra, $14 (SP) 7 | jal $2ca6d0 8 | nop 9 | !endhook: 10 | lw ra, $14 (SP) 11 | jr ra 12 | addiu sp, sp, $0018 13 | 14 | .org 0x856d0 15 | !main: 16 | addiu sp, sp, $ffe8 17 | sw ra, $14 (SP) 18 | lui a2, $8040 19 | ori a2, a2, $0000 //RAM location, update 369F40 to write this in for your own characters 20 | lui t0, $800f 21 | ori t0, t0, $0860 22 | sub a3, a2, t0 23 | 24 | lui t0, $8033 25 | lw t0, $DDC4 (T0) 26 | lw t3, $0008 (T0) 27 | bne t3, r0, !nonewmodelset 28 | nop 29 | lw t1, $0004 (T0) 30 | or at, a2, r0 31 | addiu t9, at, $7ce8 //model size 32 | !loop: 33 | lw t2, $0000 (T1) 34 | lui t3, $ffff 35 | and t3, t2, t3 36 | lui t4, $800f 37 | bne t3, t4, !skipthisasbiufbau9sif 38 | nop 39 | add t2, t2, a3 40 | !skipthisasbiufbau9sif: 41 | sw t2, $0000 (AT) 42 | addiu at, at, $4 43 | addiu t1, t1, $4 44 | bne t9, at, !loop 45 | nop 46 | lui at, $8033 47 | lw at, $ddc4 (AT) 48 | or t0, a2, r0 49 | sw t0, $0008 (at) 50 | nop 51 | jal $277f50 52 | lui a0, $0400 53 | addiu a3, a2, $2000 54 | //addiu a3, a3, $4000 55 | lui at, $0003 56 | ori at, at, $6000 //bank 04 size 57 | add a1, a3, at 58 | !copyloop: 59 | lw t2, $0000 (v0) 60 | sw t2, $0000 (A3) 61 | addiu a3, a3, $4 62 | addiu v0, v0, $4 63 | bne a1, a3, !copyloop 64 | nop 65 | or a0, a2, r0 66 | addiu a1, a2, $3000 //model size again 67 | !loppsaf: 68 | lw t2, $0000 (A0) 69 | lui at, $ffff 70 | and at, at, t2 71 | lui t3, $0400 72 | beq t3, at, !fixthispointer 73 | nop 74 | lui t3, $0401 75 | beq t3, at, !fixthispointer 76 | nop 77 | lui t3, $0403 78 | beq t3, at, !fixthispointer 79 | nop 80 | lui t3, $0402 81 | bne t3, at, !dontfix 82 | nop 83 | !fixthispointer: 84 | jal $30FFF8 85 | nop 86 | !dontfix: 87 | addiu a0, a0, $4 88 | bne a0, a1, !loppsaf 89 | nop 90 | 91 | !nonewmodelset: 92 | lui a0, $8034 93 | lhu a0, $afa0 (A0) 94 | andi a0, a0, $0020 95 | beq a0, r0, !end 96 | nop 97 | addiu a1, r0, $0002 98 | lui a0, $8036 99 | lw a0, $1158 (A0) 100 | lui a2, $1300 101 | jal $29edcc 102 | addiu a2, a2, $2a48 103 | !end: 104 | lw ra, $14 (SP) 105 | jr ra 106 | addiu sp, sp, $018 107 | 108 | //803e92b0 109 | .org 0xCAFF8 110 | !fixDL: 111 | addiu sp, sp, $ffe8 112 | sw ra, $14 (SP) 113 | addiu a3, a2, $2000 114 | //addiu a3, a3, $4000 115 | lui at, $00ff 116 | ori at, at, $ffff 117 | and a3, a3, at 118 | lui at, $0400 119 | sub a3, a3, at 120 | add t2, t2, a3 121 | sw t2, $0000 (A0) 122 | lui t3, $8000 123 | or t3, t3, t2 124 | jal $3145D4 125 | nop 126 | lw ra, $14 (SP) 127 | jr ra 128 | addiu sp, sp, $0018 129 | 130 | 131 | .org 0xCF5D4 132 | !dasfihasbuf: 133 | addiu sp, sp, $ffe8 134 | sw ra, $14 (SP) 135 | !loopfixDL: 136 | 137 | lw t4, $0000 (T3) 138 | lui at, $ff00 139 | and at, at, t4 140 | lui t5, $0300 141 | beq t5, at, !fixpointer 142 | nop 143 | lui t5, $0400 144 | beq t5, at, !fixpointer 145 | nop 146 | lui t5, $fd00 147 | bne t5, at, !Notexturefix 148 | nop 149 | !fixpointer: 150 | lw t7, $0004 (t3) 151 | lui at, $ff00 152 | and at, at, t7 153 | beq at, r0, !Notexturefix 154 | nop 155 | add t7, t7, a3 156 | sw t7, $0004 (T3) 157 | !Notexturefix: 158 | sw t3, $0010 (SP) 159 | lui t5, $0600 160 | bne t5, at, !nosubcall 161 | nop 162 | lw t2, $0004 (t3) 163 | lui at, $ff00 164 | and at, at, t2 165 | beq at, r0, !nosubcall 166 | nop 167 | add t2, t2, a3 168 | sw t2, $0004 (T3) 169 | lui t3, $8000 170 | jal $3145D4 171 | or t3, t3, t2 172 | !nosubcall: 173 | lw t3, $0010 (SP) 174 | !endoftextureloop: 175 | lw t2, $0008 (T3) 176 | lui at, $b800 177 | addiu t3, t3, $0008 178 | bne t2, at, !loopfixDL 179 | nop 180 | lw ra, $14 (SP) 181 | jr ra 182 | addiu sp, sp, $0018 -------------------------------------------------------------------------------- /src/asm/ff9800.txt: -------------------------------------------------------------------------------- 1 | .org 0x0000 2 | !main: 3 | addiu sp, sp, $ffe4 4 | sw ra, $14 (SP) 5 | lui t0, $8036 6 | lw t7, $1160 (T0) 7 | lbu t2, $0188 (T7) 8 | sll t2, t2, $08 9 | lui t1, $80ff 10 | add t1, t1, t2 11 | lbu t9, $770e (T1) 12 | lhu t8, $770c (T1) 13 | andi t8, t8, $ff0f 14 | lui t6, $80ff 15 | lhu t6, $770c (T6) 16 | andi t6, t6, $ff0f 17 | bne t8, t6, !nonsolid 18 | nop 19 | 20 | 21 | lbu t1, $7701 (T1) 22 | addiu t8, r0, $0 23 | addiu at, r0, $4 24 | bne at, t9, !notwario 25 | nop 26 | addiu t8, r0, $1 27 | !notwario: 28 | lw t9, $0180 (T7) 29 | add t9, t9, t8 30 | sw t9, $0180 (t7) 31 | lui t3, $80ff 32 | lbu t8, $5ff4 (t3) 33 | addiu at, r0, $0005 34 | bne at, t8, !regularbox 35 | nop 36 | lbu at, $7718 (T3) 37 | lbu t2, $0188 (T7) 38 | bne t2, at, !regularbox 39 | nop 40 | addiu t9, t9, $3 41 | sw t9, $0180 (T7) 42 | !regularbox: 43 | sll t1, t1, $2 44 | add t0, t3, t1 45 | addiu t0, t0, $2000 46 | lw t0, $7400 (T0) 47 | ori at, r0, $8000 48 | bne t0, at, !dontset 49 | nop 50 | sw r0, $0180 (T7) 51 | lw t1, $0134 (T7) 52 | ori at, r0, $c003 53 | bne t1, at, !dontset 54 | nop 55 | lui t0, $8034 56 | lui at, $4268 57 | sw at, $b1bc (T0) 58 | lui at, $1080 59 | ori at, at, $08A4 60 | sw at, $b17c (T0) 61 | ori t0, r0, $8000 62 | !dontset: 63 | sw t0, $0130 (T7) 64 | ori at, r0, $0008 65 | bne at, t0, !noexpansion 66 | nop 67 | lui a0, $80ff 68 | lbu a0, $5ff4 (a0) 69 | addiu at, r0, $0003 70 | beq at, a0, !noexpansion 71 | nop 72 | lw t1, $00d4 (T7) 73 | sw t1, $00c8 (T7) 74 | sw t7, $0018 (SP) 75 | addiu a1, r0, $0000 76 | or a0, t7, r0 77 | lui a2, $00ff 78 | jal $29edcc 79 | ori a2, a2, $9e80 80 | lw t7, $0018 (SP) 81 | lbu t2, $0188 (T7) 82 | sb t2, $0188 (V0) 83 | !noexpansion: 84 | lui t1, $4000 85 | bne t1, t0, !end 86 | nop 87 | lui t2, $8034 88 | lwc1 f2, $b1b0 (T2) 89 | lwc1 f4, $b1bc (T2) 90 | mtc1 r0, f8 91 | c.lt.s f4, f8 92 | nop 93 | bc1f !end 94 | nop 95 | lui at, $42c8 96 | mtc1 at, f4 97 | sub.s f2, f2, f4 98 | lwc1 f6, $00a4 (T7) 99 | c.lt.s f2, f6 100 | nop 101 | bc1t !end 102 | nop 103 | addiu at, r0, $0080 104 | sw at, $0130 (T7) 105 | sw r0, $0180 (T7) 106 | !end: 107 | lui t0, $8036 108 | lw t7, $1160 (T0) 109 | 110 | 111 | lbu t2, $0188 (T7) 112 | sll t2, t2, $08 113 | lui t1, $80ff 114 | add t1, t1, t2 115 | lhu t8, $770c (T1) 116 | 117 | andi t8, t8, $0040 118 | beq t8, r0, !nonmetalasnfiunsaf 119 | nop 120 | addiu at, r0, $0008 121 | sw at, $0130 (T7) 122 | !nonmetalasnfiunsaf: 123 | lw t1, $0134 (T7) 124 | ori at, r0, $a000 125 | bne at, t1, !skipplayid 126 | nop 127 | lbu t2, $0188 (T7) 128 | lui t3, $80ff 129 | sb t2, $5fef (T3) 130 | 131 | !skipplayid: 132 | sw r0, $0134 (T7) 133 | lui t2, $80ff 134 | lbu t2, $5ff4 (T2) 135 | addiu at, r0, $0003 136 | bne t2, at, !asbfuiasbzufasfasbzuf 137 | nop 138 | !nonsolid: 139 | addiu at, r0, $ffff 140 | sw at, $009c (t7) 141 | lui at, $8000 142 | sw at, $0130 (T7) 143 | !asbfuiasbzufasfasbzuf: 144 | lwc1 f2, $002c (T7) 145 | lui at, $4214 146 | mtc1 at, f4 147 | mul.s f4, f4, f2 148 | swc1 f2, $01f8 (T7) 149 | 150 | lui at, $4320 151 | mtc1 at, f4 152 | mul.s f4, f4, f2 153 | swc1 f2, $01fc (T7) 154 | 155 | 156 | lw ra, $14 (SP) 157 | jr ra 158 | addiu sp, sp, $001c -------------------------------------------------------------------------------- /src/asm/ff9d00.txt: -------------------------------------------------------------------------------- 1 | .org 0x000 2 | !main: 3 | lui t7, $8036 4 | lw t7, $1160 (T7) 5 | lw t1, $0134 (T7) 6 | ori at, r0, $a000 7 | bne at, t1, !skipplayid 8 | nop 9 | lbu t2, $0188 (T7) 10 | lui t3, $80ff 11 | sb t2, $5fef (T3) 12 | !skipplayid: 13 | jr ra 14 | nop -------------------------------------------------------------------------------- /src/asm/ff9d80.txt: -------------------------------------------------------------------------------- 1 | .org 0x000 2 | !main: 3 | lui t0, $80ff 4 | lbu t1, $5ff4 (T0) 5 | addiu at, r0, $5 6 | bne at, t1, !skip 7 | nop 8 | lbu t1, $7718 (T0) 9 | lbu t2, $7700 (T0) 10 | bne t1, t2, !skip 11 | nop 12 | lui t1, $8036 13 | lw t1, $1158 (T1) 14 | lui at, $4080 15 | mtc1 at, f2 16 | lwc1 f4, $002c (T1) 17 | mul.s f4, f4, f2 18 | swc1 f4, $002c (T1) 19 | swc1 f4, $0034 (T1) 20 | 21 | lwc1 f6, $0030 (T1) 22 | mul.s f6, f6, f2 23 | swc1 f6, $0030 (T1) 24 | !skip: 25 | jr a3 26 | nop -------------------------------------------------------------------------------- /src/declarations/discord-rich-presence.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'discord-rich-presence' 2 | -------------------------------------------------------------------------------- /src/declarations/winprocess.ts: -------------------------------------------------------------------------------- 1 | interface WinProcess { 2 | Process: (processId: number) => Process 3 | } 4 | export interface Process { 5 | open: () => void 6 | readMemory: (offset: number, length: number) => Buffer | number 7 | writeMemory: (offset: number, buffer: Buffer) => void 8 | } 9 | export default WinProcess 10 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | 3 | import * as path from 'path' 4 | import * as fs from 'fs' 5 | import * as RPCClientInstance from 'discord-rich-presence' 6 | 7 | import { Connector } from './Connector' 8 | import { Emulator } from './Emulator' 9 | import { Connection } from './Connection' 10 | import { ElectronSaveData } from '../models/State.model' 11 | 12 | interface Global extends NodeJS.Global { 13 | save: { 14 | appSaveData?: ElectronSaveData, 15 | appSavePath?: string, 16 | } 17 | } 18 | 19 | export let connector: Connector 20 | export let emulator: Emulator | undefined 21 | export let connection: Connection | undefined 22 | 23 | export const createEmulator = ( 24 | { processId, characterId }: 25 | { processId: number, characterId: number } 26 | ) => { 27 | try { 28 | emulator = new Emulator(processId, characterId) 29 | emulator.displayChatMessage('- Net64 connected -') 30 | } catch (err) { 31 | console.warn(err) 32 | } 33 | } 34 | 35 | export const deleteEmulator = () => { 36 | emulator = undefined 37 | } 38 | 39 | export const createConnection = ( 40 | { domain, ip, port, username, characterId }: 41 | { 42 | domain: string | undefined, ip: string | undefined, port: number | undefined, username: string, characterId: number, 43 | } 44 | ) => { 45 | connection = new Connection({ domain, ip, port, username, characterId }) 46 | } 47 | 48 | export const deleteConnection = () => { 49 | if (connection) connection.disconnect() 50 | connection = undefined 51 | } 52 | 53 | export const RPCClient = new RPCClientInstance('560060224321355811') 54 | RPCClient.on('error', (err: Error) => { 55 | if (process.env.NODE_ENV === 'development') { 56 | console.error(err) 57 | } 58 | }) 59 | 60 | export let RPCState = {} 61 | 62 | export function updateRPC (update: Record, clean?: boolean): void { 63 | if (clean) { RPCState = {} } 64 | Object.assign(RPCState, update) 65 | RPCClient.updatePresence(RPCState) 66 | } 67 | 68 | (() => { 69 | const onReady = () => { 70 | const mainWindow = new BrowserWindow({ 71 | width: process.env.NODE_ENV === 'development' ? 1400 : 670, 72 | height: 840, 73 | icon: path.join(__dirname, 'img/icon.png'), 74 | title: `Net64+ ${process.env.VERSION}`, 75 | webPreferences: { 76 | webSecurity: false, 77 | nodeIntegrationInWorker: true 78 | } 79 | }) 80 | connector = new Connector(mainWindow) 81 | updateRPC({ 82 | state: 'Ready', 83 | details: 'Ready', 84 | largeImageKey: 'net64', 85 | largeImageText: `Net64+ ${process.env.VERSION}` 86 | }) 87 | mainWindow.loadURL(path.normalize(`file://${__dirname}/index.html`)) 88 | 89 | if (process.env.NODE_ENV === 'development') { 90 | // eslint-disable-next-line @typescript-eslint/no-var-requires 91 | require('electron-debug')({ 92 | showDevTools: true 93 | }) 94 | mainWindow.webContents.openDevTools() 95 | } 96 | } 97 | 98 | app.on('ready', onReady) 99 | 100 | app.on('window-all-closed', () => { 101 | RPCClient.disconnect() 102 | app.quit() 103 | }) 104 | 105 | app.on('activate', () => { 106 | onReady() 107 | }) 108 | 109 | process.on('uncaughtException', (err: Error) => { 110 | const errorFolderPath = path.resolve(__dirname, 'error') 111 | const filePath = path.resolve( 112 | errorFolderPath, 113 | `./error_log_${new Date().toISOString().split('.')[0].replace(/:/g, '').replace(/-/g, '')}.log` 114 | ) 115 | if (!fs.existsSync(errorFolderPath)) { 116 | fs.mkdirSync(errorFolderPath) 117 | } 118 | fs.writeFileSync( 119 | filePath, 120 | `\ 121 | Here is a detailed error log of the unhandled exception that caused Net64+ to crash.\n 122 | Please report this error log on GitHub: https://github.com/tarnadas/net64plus/issues\n\n\ 123 | Error name: ${err.name}\n\ 124 | Error message: ${err.message}\n\ 125 | StackTrace: ${err.stack}` 126 | ) 127 | app.quit() 128 | }) 129 | })() 130 | -------------------------------------------------------------------------------- /src/models/Emulator.mock.ts: -------------------------------------------------------------------------------- 1 | import { FilteredEmulator } from './Emulator.model' 2 | import { Process } from '../declarations/winprocess' 3 | 4 | export const testEmulatorPid = -1337 5 | 6 | export const testEmulator: FilteredEmulator = { 7 | name: 'Test Emu', 8 | pid: testEmulatorPid, 9 | windowName: 'Test Super Mario 64 - Project64' 10 | } 11 | 12 | const MEMORY_SIZE = 0xFFFFFF 13 | const PLAYER_POS_X_OFFSET = 0xFF7706 14 | const PLAYER_POS_Y_OFFSET = 0xFF770A 15 | const PLAYER_ROTATION_OFFSET = 0xFF7708 16 | const PLAYER_COURSE_OFFSET = 0xFF770F 17 | 18 | export class TestProcess implements Process { 19 | private readonly memory = Buffer.alloc(MEMORY_SIZE) 20 | 21 | constructor () { 22 | this.memory.writeUInt32LE(0x3C1A8032, 0) 23 | this.memory.writeUInt32LE(0x275A7650, 4) 24 | this.memory.writeInt16LE(0x1000, PLAYER_POS_X_OFFSET) 25 | this.memory.writeInt16LE(-0x800, PLAYER_POS_Y_OFFSET) 26 | setInterval(this.updatePlayerLocation.bind(this), 500) 27 | } 28 | 29 | private updatePlayerLocation () { 30 | for (let offset = 0; offset <= 0x100; offset += 0x100) { 31 | this.memory.writeInt16LE( 32 | (this.memory.readInt16LE(PLAYER_POS_X_OFFSET + offset) - 1), 33 | PLAYER_POS_X_OFFSET + offset 34 | ) 35 | this.memory.writeInt16LE( 36 | (this.memory.readInt16LE(PLAYER_POS_Y_OFFSET + offset) - 1), 37 | PLAYER_POS_Y_OFFSET + offset 38 | ) 39 | this.memory.writeUInt16LE( 40 | ((this.memory.readUInt16LE(PLAYER_ROTATION_OFFSET + offset) + 0x80) % 0xFFFF), 41 | PLAYER_ROTATION_OFFSET + offset 42 | ) 43 | this.memory.writeUInt8(4, PLAYER_COURSE_OFFSET + offset) 44 | } 45 | } 46 | 47 | public open () {} 48 | 49 | public readMemory (offset: number, length: number): number | Buffer { 50 | let buffer: Buffer 51 | if (offset + length > MEMORY_SIZE) { 52 | buffer = Buffer.alloc(length) 53 | } else { 54 | buffer = this.memory.slice(offset, offset + length) 55 | } 56 | return buffer 57 | } 58 | 59 | public writeMemory (offset: number, buffer: Buffer) { 60 | this.memory.fill(buffer, offset, offset + buffer.length) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/models/Emulator.model.ts: -------------------------------------------------------------------------------- 1 | import { IPlayer } from '../../proto/ServerClientMessage' 2 | 3 | export interface FilteredEmulator { 4 | name: string 5 | pid: number 6 | windowName?: string 7 | } 8 | 9 | export interface Position { 10 | x: number 11 | y: number 12 | rotation: number 13 | course: number 14 | } 15 | 16 | export interface Player extends IPlayer { 17 | position?: Position 18 | } 19 | 20 | export const CHARACTER_IMAGES = [ 21 | 'mario.png', 22 | 'luigi.png', 23 | 'yoshi.png', 24 | 'wario.png', 25 | 'peach.png', 26 | 'toad.png', 27 | 'waluigi.png', 28 | 'rosalina.png', 29 | 'sonic.png', 30 | 'knuckles.png', 31 | 'goomba.png', 32 | 'kirby.png' 33 | ] 34 | -------------------------------------------------------------------------------- /src/models/Message.model.ts: -------------------------------------------------------------------------------- 1 | export enum MainMessage { 2 | WEBSOCKET_CLOSE = 'WEBSOCKET_CLOSE', 3 | EMULATOR_DISCONNECT = 'EMULATOR_DISCONNECT', 4 | EMULATOR_CONNECTED = 'EMULATOR_CONNECTED', 5 | UPDATE_EMULATORS = 'UPDATE_EMULATORS', 6 | SET_SERVER = 'SET_SERVER', 7 | SET_PLAYERS = 'SET_PLAYERS', 8 | SET_PLAYER = 'SET_PLAYER', 9 | SET_PLAYER_ID = 'SET_PLAYER_ID', 10 | UPDATE_PLAYER_POSITIONS = 'UPDATE_PLAYER_POSITIONS', 11 | GAME_MODE = 'GAME_MODE', 12 | SERVER_FULL = 'SERVER_FULL', 13 | WRONG_VERSION = 'WRONG_VERSION', 14 | AUTHENTICATION_ACCEPTED = 'AUTHENTICATION_ACCEPTED', 15 | AUTHENTICATION_DENIED = 'AUTHENTICATION_DENIED', 16 | CHAT_GLOBAL = 'CHAT_GLOBAL', 17 | CHAT_COMMAND = 'CHAT_COMMAND', 18 | SET_CONNECTION_ERROR = 'SET_CONNECTION_ERROR', 19 | SET_EMULATOR_ERROR = 'SET_EMULATOR_ERROR', 20 | CONSOLE_INFO = 'CONSOLE_INFO', 21 | SET_CHARACTER = 'SET_CHARACTER' 22 | } 23 | 24 | export enum RendererMessage { 25 | CREATE_CONNECTION = 'CREATE_CONNECTION', 26 | DISCONNECT = 'DISCONNECT', 27 | UPDATE_EMULATORS = 'UPDATE_EMULATORS', 28 | CREATE_EMULATOR_CONNECTION = 'CREATE_EMULATOR_CONNECTION', 29 | DISCONNECT_EMULATOR = 'DISCONNECT_EMULATOR', 30 | PLAYER_UPDATE = 'PLAYER_UPDATE', 31 | PASSWORD = 'PASSWORD', 32 | CHAT_GLOBAL = 'CHAT_GLOBAL', 33 | CHAT_COMMAND = 'CHAT_COMMAND', 34 | EMU_CHAT = 'EMU_CHAT', 35 | HOTKEYS_CHANGED = 'HOTKEYS_CHANGED', 36 | CHARACTER_CYCLING_ORDER_CHANGED = 'CHARACTER_CYCLING_ORDER_CHANGED', 37 | GAMEPAD_BUTTON_STATE_CHANGED = 'GAMEPAD_BUTTON_STATE_CHANGED' 38 | } 39 | -------------------------------------------------------------------------------- /src/models/Release.model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | interface Asset { 3 | browser_download_url: string 4 | name: string 5 | } 6 | 7 | export interface Release { 8 | draft: boolean 9 | prerelease: boolean 10 | assets: Asset[] 11 | tag_name: string 12 | body: string 13 | } 14 | -------------------------------------------------------------------------------- /src/models/Server.model.ts: -------------------------------------------------------------------------------- 1 | import { Player } from './Emulator.model' 2 | import { GameModeType } from '../../proto/ServerClientMessage' 3 | 4 | export interface Server { 5 | id?: string 6 | domain?: string 7 | ip: string 8 | port: number 9 | name?: string 10 | description?: string 11 | players?: Array 12 | countryCode?: string 13 | gameMode?: GameModeType 14 | version?: string 15 | passwordRequired?: boolean | null 16 | isDedicated?: boolean 17 | } 18 | 19 | export interface Course { 20 | short: string 21 | long: string 22 | icon: string 23 | } 24 | -------------------------------------------------------------------------------- /src/models/State.model.ts: -------------------------------------------------------------------------------- 1 | import { RouterState as ReactRouterState } from 'react-router-redux' 2 | 3 | import { Server } from './Server.model' 4 | import { FilteredEmulator, Position } from './Emulator.model' 5 | import { ChildProcess } from 'child_process' 6 | import { HotkeyShortcut } from '../main/HotkeyManager' 7 | 8 | export interface ElectronServerSaveDataDraft { 9 | name: string 10 | description: string 11 | gamemode: number 12 | enableGamemodeVote: boolean 13 | passwordRequired: boolean 14 | password: string 15 | port: number 16 | enableWebHook: boolean 17 | } 18 | export type ElectronServerSaveData = Readonly 19 | 20 | export interface ElectronSaveDataDraft { 21 | apiKey: string 22 | username: string 23 | character: number 24 | emuChat: boolean 25 | globalHotkeysEnabled: boolean 26 | hotkeyBindings: { [shortcut in HotkeyShortcut]: string[] } 27 | characterCylingOrder: Array<{characterId: number, on: boolean}> 28 | gamepadId: string | undefined 29 | lastIp: string 30 | lastPort: number 31 | version: string 32 | serverOptions: ElectronServerSaveData 33 | } 34 | export type ElectronSaveData = Readonly 35 | 36 | export interface SaveStateDraft { 37 | appSaveData: T 38 | appSavePath: string 39 | } 40 | export type SaveState = Readonly> 41 | 42 | export type RouterStateDraft = ReactRouterState 43 | export type RouterState = Readonly 44 | 45 | export interface ConnectionStateDraft { 46 | server: Server | null 47 | playerId: number | null 48 | selfPos: Position 49 | cameraAngle: number 50 | authenticated: boolean 51 | authenticationThrottle: number 52 | hasToken: boolean 53 | error: string 54 | } 55 | export type ConnectionState = Readonly 56 | 57 | export interface EmulatorStateDraft { 58 | emulators: FilteredEmulator[] 59 | isConnectedToEmulator: boolean 60 | error: string 61 | } 62 | 63 | export type EmulatorState = Readonly 64 | 65 | export enum IoChannel { 66 | Out, Warn, Err 67 | } 68 | 69 | export interface ConsoleServerMessage { 70 | key: string 71 | message: string 72 | channel: IoChannel 73 | } 74 | 75 | export interface ServerStateDraft { 76 | process: ChildProcess | null 77 | exitCode: number | null 78 | server: Server | null 79 | messages: ConsoleServerMessage[] 80 | } 81 | export type ServerState = Readonly 82 | 83 | export interface ChatMessage { 84 | key: number 85 | time: string 86 | message: string 87 | username: string 88 | isTrusted: boolean 89 | } 90 | 91 | export interface ChatStateDraft { 92 | global: ChatMessage[] 93 | } 94 | export type ChatState = Readonly 95 | 96 | export interface SnackbarStateDraft { 97 | message: string | null 98 | } 99 | export type SnackbarState = Readonly 100 | 101 | export interface StateDraft { 102 | save: SaveState 103 | router: RouterState 104 | connection: ConnectionState 105 | emulator: EmulatorState 106 | server: ServerState 107 | chat: ChatState 108 | snackbar: SnackbarState 109 | } 110 | export type State = Readonly 111 | -------------------------------------------------------------------------------- /src/renderer/GamepadManager.ts: -------------------------------------------------------------------------------- 1 | import { Connector } from './Connector' 2 | 3 | export type ButtonState = Array<{ 4 | /** The button identifier */ 5 | key: number, 6 | /** The analog value of the button */ 7 | value: number, 8 | /** Whether the button is pressed */ 9 | pressed: boolean, 10 | }> 11 | 12 | export class GamepadManager { 13 | private readonly window: Window 14 | private readonly connector: Connector 15 | private _buttonState: ButtonState | undefined = undefined 16 | private readonly _buttonStateListeners: Array<(buttonState: ButtonState) => void> = [] 17 | 18 | public selectedGamepad: Gamepad | undefined = undefined 19 | 20 | constructor (window: Window, connector: Connector, defaultGamepadId?: string) { 21 | this.window = window 22 | this.connector = connector 23 | this.window.addEventListener('gamepadconnected', (event) => { 24 | // If the default gamepad id is connected, and no other gamepad is selected, automatically bind 25 | const gamepadEvent = event as GamepadEvent 26 | if (this.selectedGamepad === undefined && gamepadEvent.gamepad.id === defaultGamepadId) { 27 | this.selectedGamepad = gamepadEvent.gamepad 28 | } 29 | }) 30 | 31 | this.updateState = this.updateState.bind(this) 32 | this.emitButtonState = this.emitButtonState.bind(this) 33 | this.getConnectedGamepads = this.getConnectedGamepads.bind(this) 34 | 35 | requestAnimationFrame(this.updateState) 36 | } 37 | 38 | updateState () { 39 | const selectedGamepad = this.selectedGamepad 40 | if (selectedGamepad) { 41 | const gamepad = this.getConnectedGamepads().find((gamepad) => 42 | (gamepad ? gamepad.id : undefined) === selectedGamepad.id 43 | ) 44 | if (gamepad) { 45 | // If this is the initializer, skip change detection 46 | if (this._buttonState === undefined) { 47 | this._buttonState = gamepad.buttons.map((gamepadButton, index) => 48 | ({ key: index, pressed: gamepadButton.pressed, value: gamepadButton.value }) 49 | ) 50 | requestAnimationFrame(this.updateState) 51 | return 52 | } 53 | 54 | // Detect changes in button state since last poll 55 | const changes: ButtonState = [] 56 | for (let index = 0; index < gamepad.buttons.length; index++) { 57 | const button = gamepad.buttons[index] 58 | const oldButton = this._buttonState[index] 59 | if (oldButton === undefined || oldButton.pressed !== button.pressed || oldButton.value !== button.value) { 60 | changes.push({ key: index, pressed: button.pressed, value: button.value }) 61 | } 62 | } 63 | if (changes.length > 0) { 64 | this._buttonState = gamepad.buttons.map((gamepadButton, index) => 65 | ({ key: index, pressed: gamepadButton.pressed, value: gamepadButton.value }) 66 | ) 67 | // Emit an event to renderer and main with the changes 68 | this.emitButtonState(changes) 69 | } 70 | } 71 | } 72 | requestAnimationFrame(this.updateState) 73 | } 74 | 75 | private emitButtonState (buttonState: ButtonState) { 76 | this.connector.emitButtonState({ buttonState }) 77 | this._buttonStateListeners.forEach((callback) => callback(buttonState)) 78 | } 79 | 80 | public addButtonStateListener (callback: ((buttonState: ButtonState) => void)) { 81 | this._buttonStateListeners.push(callback) 82 | } 83 | 84 | public removeButtonStateListener (callback: ((buttonState: ButtonState) => void)) { 85 | const index = this._buttonStateListeners.findIndex((value) => value === callback) 86 | if (index !== -1) { 87 | this._buttonStateListeners.splice(index, 1) 88 | } 89 | } 90 | 91 | public getConnectedGamepads () { 92 | return Array.from(this.window.navigator.getGamepads()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/renderer/actions/chat.ts: -------------------------------------------------------------------------------- 1 | import { AddGlobalChatMessageAction, ClearGlobalChatMessagesAction } from './models/chat.model' 2 | import { ChatMessage } from '../../models/State.model' 3 | 4 | export function addGlobalChatMessage (chatMessage: ChatMessage): AddGlobalChatMessageAction { 5 | return { 6 | type: 'ADD_GLOBAL_CHAT_MESSAGE', 7 | chatMessage 8 | } 9 | } 10 | 11 | export function clearGlobalChatMessages (): ClearGlobalChatMessagesAction { 12 | return { 13 | type: 'CLEAR_GLOBAL_CHAT_MESSAGES' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/actions/connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SetServerAction, 3 | SetConnectionErrorAction, 4 | SetPlayersAction, 5 | SetPlayerAction, 6 | SetPlayerIdAction, 7 | SetGameModeAction, 8 | AuthenticationRequired, 9 | AuthenticationAccepted, 10 | AuthenticationDenied, 11 | DisconnectAction, 12 | ConnectionActionType, 13 | UpdatePlayerPositionAction 14 | } from './models/connection.model' 15 | import { Server } from '../../models/Server.model' 16 | import { IPlayer, IPlayerUpdate } from '../../../proto/ServerClientMessage' 17 | import { Position } from '../../models/Emulator.model' 18 | 19 | export function setServer (server: Server): SetServerAction { 20 | return { 21 | type: ConnectionActionType.SET_SERVER, 22 | server 23 | } 24 | } 25 | 26 | export function setConnectionError (error: string): SetConnectionErrorAction { 27 | return { 28 | type: ConnectionActionType.SET_CONNECTION_ERROR, 29 | error 30 | } 31 | } 32 | 33 | export function setPlayers (players: IPlayerUpdate[]): SetPlayersAction { 34 | return { 35 | type: ConnectionActionType.SET_PLAYERS, 36 | players 37 | } 38 | } 39 | 40 | export function setPlayer (playerId: number, player: IPlayer): SetPlayerAction { 41 | return { 42 | type: ConnectionActionType.SET_PLAYER, 43 | playerId, 44 | player 45 | } 46 | } 47 | 48 | export function setPlayerId (playerId: number): SetPlayerIdAction { 49 | return { 50 | type: ConnectionActionType.SET_PLAYER_ID, 51 | playerId 52 | } 53 | } 54 | 55 | export function updatePlayerPositions ( 56 | { self, cameraAngle, positions }: {self: Position, cameraAngle: number, positions: Array} 57 | ): UpdatePlayerPositionAction { 58 | return { 59 | type: ConnectionActionType.UPDATE_PLAYER_POSITIONS, 60 | self, 61 | cameraAngle, 62 | positions 63 | } 64 | } 65 | 66 | export function setGameMode (gameMode: number): SetGameModeAction { 67 | return { 68 | type: ConnectionActionType.GAME_MODE, 69 | gameMode 70 | } 71 | } 72 | 73 | export function authenticationRequired (): AuthenticationRequired { 74 | return { 75 | type: ConnectionActionType.AUTHENTICATION_REQUIRED 76 | } 77 | } 78 | 79 | export function authenticationAccepted (): AuthenticationAccepted { 80 | return { 81 | type: ConnectionActionType.AUTHENTICATION_ACCEPTED 82 | } 83 | } 84 | 85 | export function authenticationDenied (throttle: number): AuthenticationDenied { 86 | return { 87 | type: ConnectionActionType.AUTHENTICATION_DENIED, 88 | throttle 89 | } 90 | } 91 | 92 | export function disconnect (): DisconnectAction { 93 | return { 94 | type: ConnectionActionType.DISCONNECT 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/renderer/actions/emulator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SetEmulatorError, EmulatorActionType, IsConnectedToEmulatorAction, UpdateEmulatorsAction 3 | } from './models/emulator.model' 4 | import { FilteredEmulator } from '../../models/Emulator.model' 5 | 6 | export function updateEmulators (emulators: FilteredEmulator[]): UpdateEmulatorsAction { 7 | return { 8 | type: EmulatorActionType.UPDATE_EMULATORS, 9 | emulators 10 | } 11 | } 12 | 13 | export function isConnectedToEmulator (isConnectedToEmulator: boolean): IsConnectedToEmulatorAction { 14 | return { 15 | type: EmulatorActionType.IS_CONNECTED_TO_EMULATOR, 16 | isConnectedToEmulator 17 | } 18 | } 19 | 20 | export function setEmulatorError (error?: string): SetEmulatorError { 21 | return { 22 | type: EmulatorActionType.SET_ERROR, 23 | error 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/actions/models/chat.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { ChatMessage } from '../../../models/State.model' 4 | 5 | export interface AddGlobalChatMessageAction extends Action { 6 | chatMessage: ChatMessage 7 | } 8 | 9 | export type ClearGlobalChatMessagesAction = Action 10 | 11 | export type ChatAction = 12 | AddGlobalChatMessageAction 13 | & ClearGlobalChatMessagesAction 14 | -------------------------------------------------------------------------------- /src/renderer/actions/models/connection.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { Server } from '../../../models/Server.model' 4 | import { IPlayer, IPlayerUpdate } from '../../../../proto/ServerClientMessage' 5 | import { Position } from '../../../models/Emulator.model' 6 | 7 | export interface SetServerAction extends Action { 8 | server: Server 9 | } 10 | 11 | export interface SetConnectionErrorAction extends Action { 12 | error: string 13 | } 14 | 15 | export interface SetPlayersAction extends Action { 16 | players: IPlayerUpdate[] 17 | } 18 | 19 | export interface SetPlayerAction extends Action { 20 | playerId: number 21 | player: IPlayer 22 | } 23 | 24 | export interface SetPlayerIdAction extends Action { 25 | playerId: number 26 | } 27 | 28 | export interface UpdatePlayerPositionAction extends Action { 29 | self: Position 30 | cameraAngle: number 31 | positions: Array 32 | } 33 | 34 | export interface SetGameModeAction extends Action { 35 | gameMode: number 36 | } 37 | 38 | export type AuthenticationRequired = Action 39 | 40 | export type AuthenticationAccepted = Action 41 | 42 | export interface AuthenticationDenied extends Action { 43 | throttle: number 44 | } 45 | 46 | export type DisconnectAction = Action 47 | 48 | export type ConnectionAction = 49 | SetServerAction 50 | & SetConnectionErrorAction 51 | & SetPlayersAction 52 | & SetPlayerAction 53 | & SetPlayerIdAction 54 | & UpdatePlayerPositionAction 55 | & SetGameModeAction 56 | & AuthenticationRequired 57 | & AuthenticationAccepted 58 | & AuthenticationDenied 59 | & DisconnectAction 60 | 61 | export enum ConnectionActionType { 62 | SET_SERVER = 'SET_SERVER', 63 | SET_CONNECTION_ERROR = 'SET_CONNECTION_ERROR', 64 | SET_PLAYERS = 'SET_PLAYERS', 65 | SET_PLAYER = 'SET_PLAYER', 66 | SET_PLAYER_ID = 'SET_PLAYER_ID', 67 | UPDATE_PLAYER_POSITIONS = 'UPDATE_PLAYER_POSITIONS', 68 | GAME_MODE = 'GAME_MODE', 69 | AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED', 70 | AUTHENTICATION_ACCEPTED = 'AUTHENTICATION_ACCEPTED', 71 | AUTHENTICATION_DENIED = 'AUTHENTICATION_DENIED', 72 | DISCONNECT = 'DISCONNECT' 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/actions/models/emulator.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { FilteredEmulator } from '../../../models/Emulator.model' 4 | 5 | export interface UpdateEmulatorsAction extends Action { 6 | emulators: FilteredEmulator[] 7 | } 8 | 9 | export interface IsConnectedToEmulatorAction extends Action { 10 | isConnectedToEmulator: boolean 11 | } 12 | 13 | export interface SetEmulatorError extends Action { 14 | error?: string 15 | } 16 | 17 | export type EmulatorAction = 18 | UpdateEmulatorsAction 19 | & IsConnectedToEmulatorAction 20 | & SetEmulatorError 21 | 22 | export enum EmulatorActionType { 23 | UPDATE_EMULATORS = 'UPDATE_EMULATORS', 24 | SET_ERROR = 'SET_EMULATOR_ERROR', 25 | IS_CONNECTED_TO_EMULATOR = 'IS_CONNECTED_TO_EMULATOR' 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/actions/models/router.model.ts: -------------------------------------------------------------------------------- 1 | import { LocationChangeAction } from 'react-router-redux' 2 | 3 | export type RouterAction = 4 | LocationChangeAction 5 | -------------------------------------------------------------------------------- /src/renderer/actions/models/save.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { ElectronServerSaveData } from '../../../models/State.model' 4 | import { HotkeyShortcut } from '../../../main/HotkeyManager' 5 | 6 | export interface SetUsernameAction extends Action { 7 | username: string 8 | } 9 | 10 | export interface SetCharacterAction extends Action { 11 | character: number 12 | } 13 | 14 | export interface SetEmuChatAction extends Action { 15 | emuChat: boolean 16 | } 17 | 18 | export interface SetGlobalHotkeysEnabledAction extends Action { 19 | globalHotkeysEnabled: boolean 20 | } 21 | 22 | export interface SetHotkeyBindingsAction extends Action { 23 | hotkeyBindings: { [shortcut in HotkeyShortcut]: string[] } 24 | } 25 | 26 | export interface SetCharacterCyclingOrderAction extends Action { 27 | characterCyclingOrder: Array<{characterId: number, on: boolean}> 28 | } 29 | 30 | export interface SetGamepadIdAction extends Action { 31 | gamepadId: string | undefined 32 | } 33 | 34 | export interface AddApiKeyAction extends Action { 35 | apiKey: string 36 | } 37 | 38 | export interface SetVersionAction extends Action { 39 | version: string 40 | } 41 | 42 | export interface SetServerOptionsAction extends Action { 43 | serverOptions: ElectronServerSaveData 44 | apiKey: string 45 | } 46 | 47 | export type SaveAction = 48 | SetUsernameAction 49 | & SetCharacterAction 50 | & SetEmuChatAction 51 | & SetGlobalHotkeysEnabledAction 52 | & SetHotkeyBindingsAction 53 | & SetCharacterCyclingOrderAction 54 | & SetGamepadIdAction 55 | & AddApiKeyAction 56 | & SetVersionAction 57 | & SetServerOptionsAction 58 | -------------------------------------------------------------------------------- /src/renderer/actions/models/server.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { ChildProcess } from 'child_process' 4 | 5 | export interface SetServerProcessAction extends Action { 6 | process: ChildProcess 7 | } 8 | 9 | export type RemoveServerProcessAction = Action 10 | 11 | export interface ExitServerProcessAction extends Action { 12 | code: number 13 | } 14 | 15 | export interface AddServerMessageAction extends Action { 16 | message: string 17 | isStdErr: boolean 18 | } 19 | 20 | export type ServerAction = 21 | SetServerProcessAction 22 | & RemoveServerProcessAction 23 | & ExitServerProcessAction 24 | & AddServerMessageAction 25 | 26 | export enum ServerActionType { 27 | SET_SERVER_PROCESS = 'SET_SERVER_PROCESS', 28 | REMOVE_SERVER_PROCESS = 'REMOVE_SERVER_PROCESS', 29 | EXIT_SERVER_PROCESS = 'EXIT_SERVER_PROCESS', 30 | ADD_SERVER_MESSAGE = 'ADD_SERVER_MESSAGE' 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/actions/models/snackbar.model.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | export interface ShowSnackbarAction extends Action { 4 | message: string 5 | } 6 | 7 | export type HideSnackbarAction = Action 8 | 9 | export type SnackbarAction = 10 | ShowSnackbarAction 11 | & HideSnackbarAction 12 | 13 | export enum SnackbarActionType { 14 | SHOW_SNACKBAR = 'SHOW_SNACKBAR', 15 | HIDE_SNACKBAR = 'HIDE_SNACKBAR' 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/actions/save.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | 3 | import { 4 | SetUsernameAction, 5 | SetCharacterAction, 6 | SetEmuChatAction, 7 | AddApiKeyAction, 8 | SetVersionAction, 9 | SetServerOptionsAction, 10 | SetGlobalHotkeysEnabledAction, 11 | SetGamepadIdAction 12 | } from './models/save.model' 13 | import { ElectronServerSaveData } from '../../models/State.model' 14 | import { HotkeyShortcut } from '../../main/HotkeyManager' 15 | 16 | export function setUsername (username: string): SetUsernameAction { 17 | return { 18 | type: 'SET_USERNAME', 19 | username 20 | } 21 | } 22 | 23 | export function setCharacter (character: number): SetCharacterAction { 24 | return { 25 | type: 'SET_CHARACTER', 26 | character 27 | } 28 | } 29 | export function setEmuChat (emuChat: boolean): SetEmuChatAction { 30 | return { 31 | type: 'SET_EMU_CHAT', 32 | emuChat 33 | } 34 | } 35 | export function setGlobalHotkeysEnabled (globalHotkeysEnabled: boolean): SetGlobalHotkeysEnabledAction { 36 | return { 37 | type: 'SET_GLOBAL_HOTKEYS', 38 | globalHotkeysEnabled 39 | } 40 | } 41 | export function setHotkeyBindings (hotkeyBindings: { [shortcut in HotkeyShortcut]: string[] }) { 42 | return { 43 | type: 'SET_HOTKEY_BINDINGS', 44 | hotkeyBindings 45 | } 46 | } 47 | export function setCharacterCyclingOrder (characterCyclingOrder: Array<{characterId: number, on: boolean}>) { 48 | return { 49 | type: 'SET_CHARACTER_CYCLING_ORDER', 50 | characterCyclingOrder 51 | } 52 | } 53 | export function setGamepadId (gamepadId: string | undefined): SetGamepadIdAction { 54 | return { 55 | type: 'SET_GAMEPAD_ID', 56 | gamepadId 57 | } 58 | } 59 | 60 | export function addApiKey (apiKey: string): AddApiKeyAction { 61 | return { 62 | type: 'ADD_API_KEY', 63 | apiKey 64 | } 65 | } 66 | 67 | export function deleteApiKey (): Action { 68 | return { 69 | type: 'DELETE_API_KEY' 70 | } 71 | } 72 | 73 | export function setVersion (version: string): SetVersionAction { 74 | return { 75 | type: 'SET_VERSION', 76 | version 77 | } 78 | } 79 | 80 | export function saveServerOptions (serverOptions: ElectronServerSaveData, apiKey: string): SetServerOptionsAction { 81 | return { 82 | type: 'SAVE_SERVER_OPTIONS', 83 | serverOptions, 84 | apiKey 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/renderer/actions/server.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process' 2 | 3 | import { 4 | SetServerProcessAction, 5 | ServerActionType, 6 | RemoveServerProcessAction, 7 | AddServerMessageAction, 8 | ExitServerProcessAction 9 | } from './models/server.model' 10 | 11 | export function setServerProcess (process: ChildProcess): SetServerProcessAction { 12 | return { 13 | type: ServerActionType.SET_SERVER_PROCESS, 14 | process 15 | } 16 | } 17 | 18 | export function removeServerProcess (): RemoveServerProcessAction { 19 | return { 20 | type: ServerActionType.REMOVE_SERVER_PROCESS 21 | } 22 | } 23 | 24 | export function exitServerProcess (code: number): ExitServerProcessAction { 25 | return { 26 | type: ServerActionType.EXIT_SERVER_PROCESS, 27 | code 28 | } 29 | } 30 | 31 | export function addServerMessage (message: string, isStdErr = false): AddServerMessageAction { 32 | return { 33 | type: ServerActionType.ADD_SERVER_MESSAGE, 34 | message, 35 | isStdErr 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/actions/snackbar.ts: -------------------------------------------------------------------------------- 1 | import { ShowSnackbarAction, SnackbarActionType, HideSnackbarAction } from './models/snackbar.model' 2 | 3 | export function showSnackbar (message: string): ShowSnackbarAction { 4 | return { 5 | type: SnackbarActionType.SHOW_SNACKBAR, 6 | message 7 | } 8 | } 9 | 10 | export function hideSnackbar (): HideSnackbarAction { 11 | return { 12 | type: SnackbarActionType.HIDE_SNACKBAR 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/components/areas/ChatArea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { connector } from '../..' 5 | import { SMMButton } from '../buttons/SMMButton' 6 | import { ChatMessagePanel } from '../panels/ChatMessagePanel' 7 | import { State, ChatMessage } from '../../../models/State.model' 8 | 9 | interface ChatAreaProps { 10 | chat: ChatMessage[] 11 | } 12 | 13 | interface ChatAreaState { 14 | message: string 15 | } 16 | 17 | const MAX_LENGTH_CHAT_MESSAGE = 100 18 | 19 | class Area extends React.PureComponent { 20 | private chat: HTMLElement | null = null 21 | 22 | constructor (public props: ChatAreaProps) { 23 | super(props) 24 | this.state = { 25 | message: '' 26 | } 27 | this.onMessageChange = this.onMessageChange.bind(this) 28 | this.onKeyPress = this.onKeyPress.bind(this) 29 | this.onSend = this.onSend.bind(this) 30 | this.renderChatMessages = this.renderChatMessages.bind(this) 31 | } 32 | 33 | componentDidUpdate (prevProps: ChatAreaProps) { 34 | if (prevProps.chat === this.props.chat || !this.chat) return 35 | this.chat.scrollTop = this.chat.scrollHeight 36 | } 37 | 38 | onMessageChange (e: React.ChangeEvent) { 39 | let value = e.target.value 40 | if (value.length > MAX_LENGTH_CHAT_MESSAGE) { 41 | value = value.substr(0, MAX_LENGTH_CHAT_MESSAGE) 42 | } 43 | this.setState({ 44 | message: value 45 | }) 46 | } 47 | 48 | onKeyPress (e: React.KeyboardEvent) { 49 | if (e.key !== 'Enter') return 50 | if (!this.state.message) return 51 | this.sendChatMessage(this.state.message) 52 | } 53 | 54 | onSend () { 55 | if (!this.state.message) return 56 | this.sendChatMessage(this.state.message) 57 | } 58 | 59 | private sendChatMessage (message: string): void { 60 | if (message[0] === '/') { 61 | const [command, ...args] = message.substr(1).split(' ') 62 | connector.sendCommandMessage(command, args) 63 | } else { 64 | connector.sendGlobalChatMessage(this.state.message) 65 | } 66 | this.setState({ 67 | message: '' 68 | }) 69 | } 70 | 71 | renderChatMessages (chat: ChatMessage[]) { 72 | return chat.map( 73 | message => 74 | message.isTrusted 75 | ?
', '

') 80 | }} 81 | /> 82 | : 86 | ) 87 | } 88 | 89 | render () { 90 | const styles: Record = { 91 | area: { 92 | display: 'flex', 93 | flexDirection: 'column', 94 | color: '#000', 95 | flex: '1 0 auto', 96 | margin: '8px', 97 | fontSize: '18px', 98 | alignItems: 'flex-start' 99 | }, 100 | header: { 101 | display: 'flex', 102 | alignItems: 'center', 103 | width: '100%', 104 | minHeight: '50px' 105 | }, 106 | chat: { 107 | display: 'flex', 108 | overflowY: 'auto', 109 | overflowX: 'hidden', 110 | flexDirection: 'column', 111 | backgroundColor: '#fff', 112 | flex: '1 1 0', 113 | width: '100%', 114 | fontFamily: 'arial', 115 | minHeight: '100px' 116 | }, 117 | input: { 118 | fontSize: '18px', 119 | flex: '1 1 auto' 120 | } 121 | } 122 | return ( 123 |

124 |
125 | 131 | 132 |
133 |
{ this.chat = x }}> 134 | { 135 | this.renderChatMessages(this.props.chat) 136 | } 137 |
138 |
139 | ) 140 | } 141 | } 142 | export const ChatArea = connect((state: State) => ({ 143 | chat: state.chat.global 144 | }))(Area) 145 | -------------------------------------------------------------------------------- /src/renderer/components/areas/ConnectionArea.scss: -------------------------------------------------------------------------------- 1 | .connection-area { 2 | overflow-y: auto; 3 | padding: 4px; 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | flex: 1 1 auto; 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/components/areas/ConnectionArea.tsx: -------------------------------------------------------------------------------- 1 | import './ConnectionArea.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { SendPasswordArea } from './SendPasswordArea' 7 | import { ServerPanel } from '../panels/ServerPanel' 8 | import { ChatArea } from '../areas/ChatArea' 9 | import { State } from '../../../models/State.model' 10 | import { Server } from '../../../models/Server.model' 11 | 12 | interface ConnectionAreaProps { 13 | dispatch: Dispatch 14 | server: Server 15 | authenticated: boolean 16 | authenticationThrottle: number 17 | } 18 | 19 | class Area extends React.PureComponent { 20 | public render (): JSX.Element { 21 | const { server, authenticated, authenticationThrottle } = this.props 22 | return ( 23 |
24 | 25 | 26 | { 27 | !authenticated && 28 | 31 | } 32 |
33 | ) 34 | } 35 | } 36 | export const ConnectionArea = connect((state: State) => ({ 37 | authenticated: state.connection.authenticated, 38 | authenticationThrottle: state.connection.authenticationThrottle 39 | }))(Area) 40 | -------------------------------------------------------------------------------- /src/renderer/components/areas/HostArea.scss: -------------------------------------------------------------------------------- 1 | .host-area { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | flex: 1 1 250px; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/components/areas/NavigationArea.scss: -------------------------------------------------------------------------------- 1 | .navigation-area { 2 | display: flex; 3 | 4 | > * { 5 | flex: 1 1 160px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/areas/NavigationArea.tsx: -------------------------------------------------------------------------------- 1 | import './NavigationArea.scss' 2 | 3 | import * as React from 'react' 4 | import { connect } from 'react-redux' 5 | import { RouterState } from 'react-router-redux' 6 | import produce from 'immer' 7 | 8 | import { NavigationButton } from '../buttons/NavigationButton' 9 | import { State } from '../../../models/State.model' 10 | 11 | interface AppLocation { 12 | link: string 13 | text: string 14 | iconSrc: string 15 | isActive: boolean 16 | isEnabled: boolean 17 | enabledCondition: (props: NavigationAreaProps) => boolean 18 | } 19 | 20 | interface NavigationAreaProps { 21 | username: string 22 | route: Readonly 23 | isConnectedToEmulator: boolean 24 | } 25 | 26 | interface NavigationAreaState { 27 | locations: AppLocation[] 28 | } 29 | 30 | class Area extends React.PureComponent { 31 | constructor (props: NavigationAreaProps) { 32 | super(props) 33 | this.state = { 34 | locations: [ 35 | { 36 | link: '/faq', 37 | text: 'FAQ', 38 | iconSrc: 'img/help.png', 39 | isActive: false, 40 | isEnabled: false, 41 | enabledCondition: () => true 42 | }, 43 | { 44 | link: '/', 45 | text: 'Home', 46 | iconSrc: 'img/home.svg', 47 | isActive: false, 48 | isEnabled: false, 49 | enabledCondition: () => true 50 | }, 51 | { 52 | link: '/emulator', 53 | text: 'Change Emulator', 54 | iconSrc: 'img/n64.svg', 55 | isActive: false, 56 | isEnabled: false, 57 | enabledCondition: (props) => !!props.username 58 | }, 59 | { 60 | link: '/browse', 61 | text: 'Browse Servers', 62 | iconSrc: 'img/browse.svg', 63 | isActive: false, 64 | isEnabled: false, 65 | enabledCondition: (props) => props.isConnectedToEmulator 66 | }, 67 | { 68 | link: '/connect', 69 | text: 'Direct Connect', 70 | iconSrc: 'img/connect.svg', 71 | isActive: false, 72 | isEnabled: false, 73 | enabledCondition: (props) => props.isConnectedToEmulator 74 | } 75 | ] 76 | } 77 | this.updateLocations = this.updateLocations.bind(this) 78 | this.renderNavigationButtons = this.renderNavigationButtons.bind(this) 79 | } 80 | 81 | public componentDidMount () { 82 | this.updateLocations() 83 | } 84 | 85 | public componentDidUpdate (prevProps: NavigationAreaProps) { 86 | if ( 87 | prevProps.username === this.props.username && 88 | prevProps.route.location === this.props.route.location && 89 | prevProps.isConnectedToEmulator === this.props.isConnectedToEmulator 90 | ) return 91 | this.updateLocations() 92 | } 93 | 94 | private updateLocations () { 95 | const locations = produce(this.state.locations, (draftLocations) => { 96 | const { route } = this.props 97 | draftLocations.forEach((location) => { 98 | location.isActive = route.location ? location.link === route.location.pathname : false 99 | location.isEnabled = location.enabledCondition(this.props) 100 | }) 101 | }) 102 | this.setState({ 103 | locations 104 | }) 105 | } 106 | 107 | private renderNavigationButtons (): JSX.Element[] { 108 | return this.state.locations.map((location) => { 109 | const { link, text, iconSrc, isActive, isEnabled } = location 110 | return 118 | }) 119 | } 120 | 121 | public render (): JSX.Element { 122 | return ( 123 |
126 | { this.renderNavigationButtons() } 127 |
128 | ) 129 | } 130 | } 131 | 132 | export const NavigationArea = connect((state: State) => ({ 133 | username: state.save.appSaveData.username, 134 | route: state.router, 135 | isConnectedToEmulator: state.emulator.isConnectedToEmulator 136 | }))(Area) 137 | -------------------------------------------------------------------------------- /src/renderer/components/areas/SendPasswordArea.scss: -------------------------------------------------------------------------------- 1 | .send-password-area { 2 | &-wrapper { 3 | position: fixed; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | background-color: rgba(0,0,0,0.6); 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | z-index: 101; 13 | } 14 | 15 | display: flex; 16 | flex-wrap: wrap; 17 | align-items: center; 18 | width: 80%; 19 | background-color: #24997e; 20 | border-radius: 8px; 21 | border: 5px solid black; 22 | padding: 16px; 23 | font-size: 16px; 24 | 25 | & > * { 26 | flex: 1 0 auto; 27 | margin: 12px 0; 28 | max-width: 100%; 29 | } 30 | 31 | &-header { 32 | font-size: 24px; 33 | text-align: center; 34 | } 35 | 36 | &-input { 37 | display: flex; 38 | justify-content: space-between; 39 | flex-wrap: wrap; 40 | width: 100%; 41 | 42 | label { 43 | margin-right: 20px; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/components/areas/SendPasswordArea.tsx: -------------------------------------------------------------------------------- 1 | import './SendPasswordArea.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { connector } from '../..' 7 | import { SMMButton } from '../buttons/SMMButton' 8 | import { disconnect } from '../../actions/connection' 9 | import { State } from '../../../models/State.model' 10 | 11 | export const MIN_LENGTH_PASSWORD = 4 12 | export const MAX_LENGTH_PASSWORD = 30 13 | 14 | interface SendPasswordProps { 15 | dispatch: Dispatch 16 | throttle: number 17 | } 18 | 19 | interface SendPasswordAreaState { 20 | password: string 21 | timeout: string 22 | } 23 | 24 | class Area extends React.PureComponent { 25 | private throttleUpdateInterval?: NodeJS.Timer 26 | 27 | constructor (props: SendPasswordProps) { 28 | super(props) 29 | this.state = { 30 | password: '', 31 | timeout: '' 32 | } 33 | this.onPasswordChange = this.onPasswordChange.bind(this) 34 | this.onKeyPress = this.onKeyPress.bind(this) 35 | this.onSubmit = this.onSubmit.bind(this) 36 | this.onDisconnect = this.onDisconnect.bind(this) 37 | } 38 | 39 | public componentDidMount (): void { 40 | if (!this.props.throttle || this.throttleUpdateInterval) return 41 | this.startThrottleUpdate() 42 | } 43 | 44 | // eslint-disable-next-line 45 | public componentWillReceiveProps (nextProps: SendPasswordProps): void { 46 | if (nextProps.throttle == null || this.throttleUpdateInterval) return 47 | this.startThrottleUpdate() 48 | } 49 | 50 | private startThrottleUpdate (): void { 51 | this.throttleUpdateInterval = setInterval(() => { 52 | const { throttle } = this.props 53 | const remainingThrottleTime = (throttle - Date.now()) / 1000 54 | if (remainingThrottleTime <= 0) { 55 | this.setState({ 56 | timeout: '' 57 | }) 58 | return 59 | } 60 | const timeout = String(Math.trunc(Math.ceil(remainingThrottleTime))) 61 | this.setState({ 62 | timeout 63 | }) 64 | }, 100) 65 | } 66 | 67 | public componentWillUnmount (): void { 68 | if (!this.throttleUpdateInterval) return 69 | clearInterval(this.throttleUpdateInterval) 70 | delete this.throttleUpdateInterval 71 | } 72 | 73 | private onPasswordChange ({ target }: React.ChangeEvent): void { 74 | let value = target.value 75 | if (value.length > MAX_LENGTH_PASSWORD) { 76 | value = value.substr(0, MAX_LENGTH_PASSWORD) 77 | } 78 | this.setState({ 79 | password: value 80 | }) 81 | } 82 | 83 | private onKeyPress ({ key }: React.KeyboardEvent): void { 84 | if (key === 'Enter') { 85 | this.onSubmit() 86 | } 87 | } 88 | 89 | private onSubmit (): void { 90 | const { password } = this.state 91 | connector.sendPassword(password) 92 | } 93 | 94 | private onDisconnect (): void { 95 | this.props.dispatch(disconnect()) 96 | connector.disconnect() 97 | } 98 | 99 | public render (): JSX.Element { 100 | const { password, timeout } = this.state 101 | return ( 102 |
103 |
104 |
105 | This server is password protected 106 |
107 |
108 | 109 | 115 | { 116 | timeout && 117 | 118 | } 119 |
120 | 126 | 136 |
137 |
138 | ) 139 | } 140 | } 141 | export const SendPasswordArea = connect()(Area) 142 | -------------------------------------------------------------------------------- /src/renderer/components/areas/ServerArea.scss: -------------------------------------------------------------------------------- 1 | .server-area { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | padding: 4px; 6 | flex: 1 0 auto; 7 | 8 | &-fetch { 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: space-around; 12 | 13 | & div { 14 | display: flex; 15 | justify-content: center; 16 | font-size: 18px; 17 | margin: 10px 0; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/renderer/components/areas/ServerHostArea.scss: -------------------------------------------------------------------------------- 1 | .server-host-area { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1 1 auto; 5 | margin: 0 2rem 2rem 2rem; 6 | 7 | &-buttons { 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/components/areas/ServerHostArea.tsx: -------------------------------------------------------------------------------- 1 | import './ServerHostArea.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { SMMButton } from '../buttons/SMMButton' 7 | import { ConsolePanel } from '../panels/ConsolePanel' 8 | import { removeServerProcess } from '../../actions/server' 9 | import { State, ConsoleServerMessage } from '../../../models/State.model' 10 | 11 | interface ServerHostAreaProps { 12 | dispatch: Dispatch 13 | exitCode: number | null 14 | messages: ConsoleServerMessage[] 15 | onRestart: () => void 16 | } 17 | 18 | class Area extends React.PureComponent { 19 | constructor (props: ServerHostAreaProps) { 20 | super(props) 21 | this.onClose = this.onClose.bind(this) 22 | this.onRestart = this.onRestart.bind(this) 23 | } 24 | 25 | private onClose (): void { 26 | this.props.dispatch(removeServerProcess()) 27 | } 28 | 29 | private onRestart (): void { 30 | this.props.onRestart() 31 | } 32 | 33 | public render (): JSX.Element { 34 | const { exitCode, messages } = this.props 35 | return ( 36 |
37 | 40 |
41 | 51 | { 52 | exitCode != null && 53 | 63 | } 64 |
65 |
66 | ) 67 | } 68 | } 69 | export const ServerHostArea = connect((state: State) => ({ 70 | messages: state.server.messages 71 | }))(Area) 72 | -------------------------------------------------------------------------------- /src/renderer/components/areas/TopBarArea.scss: -------------------------------------------------------------------------------- 1 | .top-bar-area { 2 | width: 100%; 3 | font-size: 12px; 4 | flex: 0 0 auto; 5 | position: relative; 6 | 7 | &-settings { 8 | position: absolute; 9 | z-index: 100; 10 | top: 44px; 11 | left: 6px; 12 | min-width: auto !important; 13 | } 14 | 15 | &-host { 16 | position: absolute; 17 | z-index: 100; 18 | top: 44px; 19 | right: 6px; 20 | min-width: auto !important; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/areas/TopBarArea.tsx: -------------------------------------------------------------------------------- 1 | import './TopBarArea.scss' 2 | 3 | import * as React from 'react' 4 | import { connect } from 'react-redux' 5 | import { ChildProcess } from 'child_process' 6 | 7 | import { SMMButton, ColorScheme } from '../buttons/SMMButton' 8 | import { NavigationArea } from './NavigationArea' 9 | import { State } from '../../../models/State.model' 10 | 11 | interface TopBarAreaProps { 12 | serverProcess: ChildProcess | null 13 | exitCode: number | null 14 | } 15 | 16 | export class Area extends React.PureComponent { 17 | public render (): JSX.Element { 18 | const { serverProcess, exitCode } = this.props 19 | let colorScheme: ColorScheme = 'yellow' 20 | if (serverProcess) { 21 | colorScheme = 'green' 22 | } 23 | if (exitCode != null) { 24 | colorScheme = 'red' 25 | } 26 | return ( 27 |
28 | 34 | 41 | 42 |
43 | ) 44 | } 45 | } 46 | export const TopBarArea = connect((state: State) => ({ 47 | serverProcess: state.server.process, 48 | exitCode: state.server.exitCode 49 | }))(Area) 50 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/HotkeyButton.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/net64plus/2793ed601dc941e22d5270850ce22cf27109b70d/src/renderer/components/buttons/HotkeyButton.scss -------------------------------------------------------------------------------- /src/renderer/components/buttons/NavigationButton.scss: -------------------------------------------------------------------------------- 1 | $button-color-primary: #ffe500; 2 | $button-color-active: #daaa26; 3 | $button-color-hover: #c0951e; 4 | $button-color-disabled: #8f8f8f; 5 | 6 | .navigation-button { 7 | display: flex; 8 | height: 36px; 9 | min-height: 36px; 10 | line-height: 36px; 11 | background-color: $button-color-primary; 12 | cursor: pointer; 13 | padding-right: 10px; 14 | text-decoration: none; 15 | position: relative; 16 | 17 | &:not(:last-child) { 18 | margin-right: 18px; 19 | } 20 | 21 | &:not(:last-child):after { 22 | position: absolute; 23 | left: calc(100% - 18px); 24 | top: 5px; 25 | width: 26px; 26 | height: 26px; 27 | content: ''; 28 | transform: rotate(45deg); 29 | box-shadow: 4px -4px 4px -2px rgba(0,0,0,0.75); 30 | background-color: $button-color-primary; 31 | z-index: 100; 32 | } 33 | 34 | &:not(:first-child):before { 35 | position: absolute; 36 | left: -18px; 37 | width: 18px; 38 | height: 36px; 39 | content: ''; 40 | background-color: $button-color-primary; 41 | } 42 | 43 | &:hover { 44 | background-color: $button-color-hover; 45 | 46 | &:not(:last-child):after { 47 | background-color: $button-color-hover; 48 | } 49 | 50 | &:not(:first-child):before { 51 | background-color: $button-color-hover; 52 | } 53 | } 54 | 55 | img { 56 | width: 100%; 57 | height: 100%; 58 | } 59 | 60 | &-icon { 61 | margin: 4px; 62 | width: 28px; 63 | height: 28px; 64 | padding: 4px; 65 | } 66 | 67 | &-label { 68 | overflow: hidden; 69 | position: relative; 70 | width: 100%; 71 | z-index: 101; 72 | 73 | div { 74 | position: absolute; 75 | bottom: 0; 76 | } 77 | } 78 | 79 | &-active { 80 | background-color: $button-color-active; 81 | 82 | &:not(:last-child):after { 83 | background-color: $button-color-active; 84 | } 85 | 86 | &:not(:first-child):before { 87 | background-color: $button-color-active; 88 | } 89 | } 90 | 91 | &-disabled { 92 | background-color: $button-color-disabled; 93 | pointer-events: none; 94 | cursor: default; 95 | 96 | &:not(:last-child):after { 97 | background-color: $button-color-disabled; 98 | pointer-events: none; 99 | cursor: default; 100 | } 101 | 102 | &:not(:first-child):before { 103 | background-color: $button-color-disabled; 104 | pointer-events: none; 105 | cursor: default; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/NavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import './NavigationButton.scss' 2 | 3 | import * as React from 'react' 4 | import { Link } from 'react-router-dom' 5 | 6 | interface NavigationButtonProps { 7 | text: string 8 | iconSrc: string 9 | link: string 10 | isActive: boolean 11 | isEnabled: boolean 12 | } 13 | 14 | export const NavigationButton = (props: NavigationButtonProps) => 15 | 23 |
24 | 25 |
26 |
27 |
{ props.text }
28 |
29 | 30 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/SMMButton.scss: -------------------------------------------------------------------------------- 1 | .smm-button { 2 | &-disabled { 3 | background-color: #666 !important; 4 | pointer-events: none; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface ToggleButtonProps { 4 | on?: boolean 5 | onText?: string 6 | offText?: string 7 | onClick?: (toggled: boolean) => void 8 | } 9 | 10 | interface ToggleButtonState { 11 | on: boolean 12 | } 13 | 14 | export class ToggleButton extends React.PureComponent { 15 | constructor (props: ToggleButtonProps) { 16 | super(props) 17 | this.state = { 18 | on: !!props.on 19 | } 20 | this.onClick = this.onClick.bind(this) 21 | } 22 | 23 | componentDidUpdate (prevProps: ToggleButtonProps) { 24 | const { on } = this.props 25 | if (on !== undefined && prevProps.on !== this.props.on) { 26 | this.setState({ on }) 27 | } 28 | } 29 | 30 | onClick () { 31 | const on = !this.state.on 32 | this.setState({ on }) 33 | if (this.props.onClick) { 34 | this.props.onClick(on) 35 | } 36 | } 37 | 38 | render () { 39 | const { onText, offText } = this.props 40 | const { on } = this.state 41 | const styles = { 42 | button: { 43 | flex: '0 0 auto', 44 | alignItems: 'center', 45 | justifyContent: 'center', 46 | lineHeight: '40px', 47 | minWidth: '120px', 48 | height: '40px', 49 | color: !on ? '#FFF' : '#000', 50 | backgroundColor: !on ? '#323245' : '#ffe500', 51 | textAlign: 'center', 52 | cursor: 'pointer', 53 | outline: 'none', 54 | overflow: 'hidden', 55 | whiteSpace: 'nowrap', 56 | boxSizing: 'border-box', 57 | border: '0', 58 | borderRadius: '5px', 59 | boxShadow: '1px 4px 13px 0 rgba(0,0,0,0.5)', 60 | display: 'inline-block', 61 | fontSize: '13px' 62 | } 63 | } as const 64 | return ( 65 |
69 | {this.props.children} 70 | {on 71 | ?
{onText ?? ''}
72 | :
{offText ?? ''}
73 | } 74 |
75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/renderer/components/dialogs/NewVersionDialog.scss: -------------------------------------------------------------------------------- 1 | .new-version-dialog { 2 | display: flex; 3 | flex-wrap: wrap; 4 | width: 80%; 5 | background-color: #24997e; 6 | border-radius: 8px; 7 | border: 5px solid black; 8 | padding: 16px; 9 | 10 | &-wrapper { 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | background-color: rgba(0,0,0,0.6); 20 | z-index: 101; 21 | } 22 | 23 | &-description { 24 | width: 100%; 25 | } 26 | 27 | &-progress { 28 | display: flex; 29 | width: 100%; 30 | margin: 10px 0; 31 | 32 | progress { 33 | flex: 1 1 auto; 34 | } 35 | 36 | div { 37 | display: flex; 38 | align-items: center; 39 | padding: 0 10px; 40 | white-space: nowrap; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/components/forms/HostForm.scss: -------------------------------------------------------------------------------- 1 | .host-form { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | padding: 0 40px 20px 40px; 6 | font-size: 16px; 7 | flex: 1 1 auto; 8 | overflow-y: auto; 9 | 10 | > * { 11 | flex: 0 0 auto; 12 | } 13 | 14 | &-field { 15 | font-size: 16px; 16 | 17 | * { 18 | font-size: 16px; 19 | } 20 | } 21 | 22 | &-wrapper { 23 | display: flex; 24 | 25 | > * { 26 | flex: 1 0 200px; 27 | min-width: 40%; 28 | margin: 0.8rem 1rem; 29 | } 30 | 31 | > *:not(:last-child) { 32 | padding-right: 1rem; 33 | } 34 | 35 | > *:not(:first-child) { 36 | padding-left: 1rem; 37 | } 38 | } 39 | 40 | &-description { 41 | display: flex; 42 | flex-direction: column; 43 | flex: 1 0 auto; 44 | margin: 0.8rem 1rem; 45 | min-height: 80px; 46 | } 47 | 48 | input:not([type=checkbox]), textarea { 49 | width: 100%; 50 | } 51 | 52 | input[type=checkbox] { 53 | width: 18px; 54 | height: 18px; 55 | } 56 | 57 | textarea { 58 | flex: 1 0 auto; 59 | resize: none; 60 | } 61 | 62 | &-button { 63 | width: 100%; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/components/headers/HostHeader.scss: -------------------------------------------------------------------------------- 1 | .host-header { 2 | display: flex; 3 | justify-content: center; 4 | margin: 1rem 0; 5 | font-size: 24px; 6 | position: initial; 7 | 8 | &-version { 9 | position: absolute; 10 | top: 6px; 11 | right: 66px; 12 | color: lightgray; 13 | font-size: 11px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/components/headers/HostHeader.tsx: -------------------------------------------------------------------------------- 1 | import './HostHeader.scss' 2 | 3 | import * as React from 'react' 4 | 5 | interface HostHeaderProps { 6 | version: string 7 | } 8 | 9 | export class HostHeader extends React.PureComponent { 10 | public render (): JSX.Element { 11 | const { version } = this.props 12 | return ( 13 |
14 | Host Net64+ Server 15 |
v{ version }
16 |
17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/components/helpers/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shell } from 'electron' 3 | 4 | interface ExternalLinkProps extends React.Props { 5 | href: string 6 | } 7 | 8 | export class ExternalLink extends React.PureComponent { 9 | constructor (public props: ExternalLinkProps) { 10 | super(props) 11 | this.onClick = this.onClick.bind(this) 12 | } 13 | 14 | onClick () { 15 | shell.openExternal(this.props.href) 16 | } 17 | 18 | render () { 19 | return 25 | { this.props.children } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/helpers/ProgressSpinner.scss: -------------------------------------------------------------------------------- 1 | .progress-spinner { 2 | width: 80px; 3 | height: 80px; 4 | border-radius: 50%; 5 | border: 7px solid transparent; 6 | border-top-color: rgba(0,0,0,0.6); 7 | animation: rotate 800ms linear infinite; 8 | 9 | &-wrapper { 10 | display: flex; 11 | width: 100%; 12 | align-items: center; 13 | justify-content: center; 14 | 15 | &:not(&-inline) { 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | height: 100%; 20 | z-index: 100; 21 | background-color: rgba(0,0,0,0.3); 22 | } 23 | } 24 | } 25 | 26 | @keyframes rotate { 27 | 0% { 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/components/helpers/ProgressSpinner.tsx: -------------------------------------------------------------------------------- 1 | import './ProgressSpinner.scss' 2 | 3 | import * as React from 'react' 4 | 5 | interface ProgressSpinnerProps { 6 | inline?: boolean 7 | } 8 | 9 | export class ProgressSpinner extends React.PureComponent { 10 | public render (): JSX.Element { 11 | const { inline } = this.props 12 | return ( 13 |
14 |
15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/panels/ChatMessagePanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { ChatMessage } from '../../../models/State.model' 4 | 5 | interface ChatMessagePanelProps { 6 | message: ChatMessage 7 | } 8 | 9 | export class ChatMessagePanel extends React.PureComponent { 10 | public render (): JSX.Element { 11 | const { message } = this.props 12 | return ( 13 |
14 | { 15 | `[${message.time}] ${message.username}: ${message.message}` 16 | } 17 |
18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/components/panels/ConsolePanel.scss: -------------------------------------------------------------------------------- 1 | .console-panel { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1 1 auto; 5 | min-height: 100px; 6 | 7 | &-messages { 8 | flex: 1 1 auto; 9 | margin: 1rem 0; 10 | background: black; 11 | color: white; 12 | overflow-y: auto; 13 | padding: 0.6rem; 14 | border-radius: 0.6rem; 15 | border: 1px solid rgba(255,255,255,0.4); 16 | box-shadow: 0px 0px 0px 2px rgba(255,255,255,0.4); 17 | } 18 | 19 | &-message { 20 | &-warning { 21 | color: yellow; 22 | } 23 | 24 | &-error { 25 | color: rgb(255, 91, 91); 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/renderer/components/panels/ConsolePanel.tsx: -------------------------------------------------------------------------------- 1 | import './ConsolePanel.scss' 2 | 3 | import * as React from 'react' 4 | 5 | import { ConsoleServerMessage, IoChannel } from '../../../models/State.model' 6 | 7 | interface ConsolePanelProps { 8 | messages: ConsoleServerMessage[] 9 | } 10 | 11 | export class ConsolePanel extends React.PureComponent { 12 | private renderConsoleMessages (): JSX.Element[] { 13 | const { messages } = this.props 14 | return messages.map(message => { 15 | let className = '' 16 | switch (message.channel) { 17 | case IoChannel.Warn: 18 | className = 'console-panel-message-warning' 19 | break 20 | case IoChannel.Err: 21 | className = 'console-panel-message-error' 22 | break 23 | } 24 | return
28 | { message.message } 29 |
30 | }) 31 | } 32 | 33 | public render (): JSX.Element { 34 | return ( 35 |
36 |
37 | { this.renderConsoleMessages() } 38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/components/panels/RadarPanel.scss: -------------------------------------------------------------------------------- 1 | .radar-panel { 2 | margin: 6px 12px; 3 | 4 | &-stroke { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | } 9 | 10 | &-icon { 11 | &-wrapper { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | overflow: visible; 16 | border-radius: 50%; 17 | position: absolute; 18 | background-color: rgba(160, 70, 70, 0.7); 19 | } 20 | } 21 | 22 | &-label { 23 | position: absolute; 24 | margin-top: 16px; 25 | font-size: 10px; 26 | border: 1px solid rgba(0, 0, 0, 0.4) ; 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | max-width: 100px; 31 | } 32 | 33 | &-range { 34 | width: calc(100% - 8px); 35 | margin: 4px; 36 | -webkit-appearance: none; 37 | background: transparent; 38 | 39 | 40 | &::-webkit-slider-thumb { 41 | -webkit-appearance: none; 42 | border: 1px solid #000000; 43 | height: 24px; 44 | width: 12px; 45 | border-radius: 3px; 46 | background: #ffffff; 47 | cursor: pointer; 48 | margin-top: -8px; 49 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 50 | } 51 | 52 | &::-webkit-slider-runnable-track { 53 | width: 100%; 54 | height: 8.4px; 55 | cursor: pointer; 56 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 57 | background: #3071a9; 58 | border-radius: 1.3px; 59 | border: 0.2px solid #010101; 60 | } 61 | 62 | &:focus::-webkit-slider-runnable-track { 63 | background: #367ebd; 64 | } 65 | 66 | &:focus { 67 | outline: none; 68 | } 69 | } 70 | 71 | &-view-indicator { 72 | position: absolute; 73 | width: 40px; 74 | height: 40px; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/components/panels/ServerPanel.scss: -------------------------------------------------------------------------------- 1 | .server-panel { 2 | font-size: 18px; 3 | margin: 10px 0; 4 | 5 | &-header { 6 | width: 100%; 7 | padding: 6px 12px; 8 | background-color: #fff8af; 9 | border-radius: 6px; 10 | border: 4px solid #f8ca00; 11 | box-shadow: 0 0 0 4px black; 12 | cursor: pointer; 13 | display: flex; 14 | flex-wrap: wrap; 15 | 16 | &-img { 17 | height: 18px; 18 | width: 27px; 19 | 20 | > img { 21 | height: 100%; 22 | } 23 | } 24 | } 25 | 26 | &-details { 27 | display: flex; 28 | flex-wrap: wrap; 29 | overflow: hidden; 30 | 31 | &-wrapper { 32 | display: flex; 33 | flex-direction: column; 34 | margin: 4px 10px 0 10px; 35 | width: calc(100% - 20px); 36 | background-color: rgba(255,255,255,0.3); 37 | border-radius: 0 0 10px 10px; 38 | } 39 | 40 | &-radar { 41 | flex: 0 0 160px; 42 | } 43 | 44 | &-playerlist { 45 | display: flex; 46 | flex: 1 1 100px; 47 | flex-wrap: wrap; 48 | align-items: flex-start; 49 | align-content: flex-start; 50 | padding: 6px; 51 | max-height: 340px; 52 | overflow: hidden; 53 | } 54 | } 55 | 56 | &-description { 57 | flex: 1 1 auto; 58 | overflow: hidden; 59 | 60 | &-inactive { 61 | display: none; 62 | } 63 | 64 | &-toggle { 65 | display: flex; 66 | align-items: center; 67 | min-width: 24px; 68 | max-width: 24px; 69 | min-height: 50px; 70 | max-height: 100%; 71 | cursor: pointer; 72 | border-radius: 4px; 73 | background-color: rgba(255,255,255,0.1); 74 | 75 | & > img { 76 | transition: 0.3s linear transform; 77 | } 78 | &-inactive > img { 79 | transform: rotate(180deg); 80 | } 81 | 82 | &:hover { 83 | background-color: rgba(255,255,255,0.3); 84 | } 85 | } 86 | } 87 | 88 | &-player { 89 | display: flex; 90 | flex: 1 1 0; 91 | border-bottom: 1px solid black; 92 | border-top: 1px solid black; 93 | min-width: 50%; 94 | overflow: hidden; 95 | 96 | &-course { 97 | min-width: 60px; 98 | max-width: 60px; 99 | margin: 0 3px; 100 | } 101 | 102 | &-img { 103 | width: 24px; 104 | height: 24px; 105 | 106 | & img { 107 | height: 100%; 108 | } 109 | } 110 | 111 | &-name { 112 | flex: 1 1 auto; 113 | white-space: nowrap; 114 | overflow: hidden; 115 | text-overflow: ellipsis; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/renderer/components/panels/SnackbarPanel.scss: -------------------------------------------------------------------------------- 1 | .snackbar-panel { 2 | position: fixed; 3 | left: 0; 4 | bottom: 60px; 5 | height: 46px; 6 | width: 250px; 7 | max-width: 70%; 8 | background-color: black; 9 | color: white; 10 | padding: 8px 12px; 11 | line-height: 30px; 12 | z-index: 1000; 13 | border-radius: 0 6px 6px 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/components/panels/SnackbarPanel.tsx: -------------------------------------------------------------------------------- 1 | import './SnackbarPanel.scss' 2 | 3 | import * as React from 'react' 4 | 5 | interface SnackBarPanelProps { 6 | message: string | null 7 | } 8 | 9 | export class SnackbarPanel extends React.PureComponent { 10 | public render (): JSX.Element { 11 | const { message } = this.props 12 | return ( 13 |
14 | { message } 15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/panels/WarningPanel.scss: -------------------------------------------------------------------------------- 1 | .warning-panel { 2 | color: #a00003; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | &-wrapper { 8 | display: flex; 9 | justify-content: center; 10 | margin: 0 20px; 11 | font-size: 18px; 12 | width: calc(100% - 40px); 13 | } 14 | 15 | & img { 16 | height: 30px; 17 | margin-right: 20px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/renderer/components/panels/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import './WarningPanel.scss' 2 | 3 | import * as React from 'react' 4 | 5 | interface WarningPanelProps { 6 | warning: string 7 | } 8 | 9 | export class WarningPanel extends React.PureComponent { 10 | render () { 11 | const { warning } = this.props 12 | return ( 13 |
14 | { 15 | warning && 16 |
17 | 18 |
{ warning }
19 |
20 | } 21 |
22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/views/AboutView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export class AboutView extends React.PureComponent { 4 | render () { 5 | const styles: Record = { 6 | view: { 7 | display: 'flex', 8 | flexDirection: 'column', 9 | backgroundColor: '#24997e', 10 | flex: '1 1 auto', 11 | color: '#000', 12 | alignItems: 'center', 13 | fontSize: '18px', 14 | overflow: 'auto', 15 | padding: '20px 40px' 16 | }, 17 | text: { 18 | paddingLeft: '14px' 19 | }, 20 | link: { 21 | cursor: 'pointer', 22 | color: '#227' 23 | } 24 | } 25 | return ( 26 |
27 |

Credits

28 |
29 |

Net64 Online Team

30 |
31 | Kaze Emanuar
32 | MelonSpeedruns
33 | Guad
34 | merlish 35 |
36 |

Net64+

37 |
38 | Tarnadas and all contributors 39 |
40 |

Luigi 3D Model

41 |
42 | Cjes
43 | GeoshiTheRed 44 |
45 |

Toad, Rosalina and Peach 3D Models

46 |
47 | AnkleD 48 |
49 |

New Character 3D Models

50 |
51 | Marshivolt 52 |
53 |

Character Head Icons

54 |
55 | Quasmok 56 |
57 |
58 |

License

59 | 60 | MIT License

61 | 62 | Copyright (c) 2017-2019 Mario Reder

63 | 64 | Permission is hereby granted, free of charge, to any person obtaining a copy 65 | of this software and associated documentation files (the "Software"), to deal 66 | in the Software without restriction, including without limitation the rights 67 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 68 | copies of the Software, and to permit persons to whom the Software is 69 | furnished to do so, subject to the following conditions:

70 | 71 | The above copyright notice and this permission notice shall be included in all 72 | copies or substantial portions of the Software.

73 | 74 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 75 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 76 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 77 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 78 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 79 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 80 | SOFTWARE. 81 |
82 |
83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/renderer/components/views/BrowseView.scss: -------------------------------------------------------------------------------- 1 | .browse-view { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: #24997e; 5 | flex: 1 1 auto; 6 | overflow: auto; 7 | 8 | h1 { 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/components/views/BrowseView.tsx: -------------------------------------------------------------------------------- 1 | import './BrowseView.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { ConnectionArea } from '../areas/ConnectionArea' 7 | import { ServerArea } from '../areas/ServerArea' 8 | import { setConnectionError } from '../../actions/connection' 9 | import { State } from '../../../models/State.model' 10 | import { Server } from '../../../models/Server.model' 11 | 12 | interface BrowseViewProps { 13 | dispatch: Dispatch 14 | server: Server 15 | connectionError: string 16 | } 17 | 18 | class View extends React.PureComponent { 19 | componentDidMount () { 20 | this.props.dispatch(setConnectionError('')) 21 | } 22 | 23 | render () { 24 | const server = this.props.server 25 | const connectionError = this.props.connectionError 26 | return ( 27 |
28 |

Browse Servers

29 | { 30 | server && !connectionError 31 | ? 32 | : 33 | } 34 |
35 | ) 36 | } 37 | } 38 | export const BrowseView = connect((state: State) => ({ 39 | server: state.connection.server, 40 | connectionError: state.connection.error 41 | }))(View) 42 | -------------------------------------------------------------------------------- /src/renderer/components/views/ConnectView.scss: -------------------------------------------------------------------------------- 1 | .connect-view { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: #24997e; 5 | flex: 1 1 auto; 6 | 7 | h1 { 8 | text-align: center; 9 | } 10 | } -------------------------------------------------------------------------------- /src/renderer/components/views/ConnectView.tsx: -------------------------------------------------------------------------------- 1 | import './ConnectView.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { ConnectionArea } from '../areas/ConnectionArea' 7 | import { ConnectArea } from '../areas/ConnectArea' 8 | import { setConnectionError } from '../../actions/connection' 9 | import { State } from '../../../models/State.model' 10 | import { Server } from '../../../models/Server.model' 11 | 12 | interface ConnectViewProps { 13 | dispatch: Dispatch 14 | server: Server 15 | connectionError: string 16 | } 17 | 18 | class View extends React.PureComponent { 19 | public componentDidMount (): void { 20 | this.props.dispatch(setConnectionError('')) 21 | } 22 | 23 | public render (): JSX.Element { 24 | const { server, connectionError } = this.props 25 | return ( 26 |
27 |

Direct Connect

28 | { 29 | server 30 | ? 31 | : 32 | } 33 |
34 | ) 35 | } 36 | } 37 | export const ConnectView = connect((state: State) => ({ 38 | server: state.connection.server, 39 | connectionError: state.connection.error 40 | }))(View) 41 | -------------------------------------------------------------------------------- /src/renderer/components/views/FaqView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { connect, Dispatch } from 'react-redux' 4 | 5 | import { SMMButton } from '../buttons/SMMButton' 6 | import { setVersion } from '../../actions/save' 7 | import { State } from '../../../models/State.model' 8 | import { ExternalLink } from '../helpers/ExternalLink' 9 | 10 | interface FaqViewProps { 11 | dispatch: Dispatch 12 | } 13 | 14 | class View extends React.PureComponent { 15 | public componentDidMount (): void { 16 | this.props.dispatch(setVersion(process.env.VERSION ?? '')) 17 | } 18 | 19 | public render (): JSX.Element { 20 | const styles: Record = { 21 | view: { 22 | display: 'flex', 23 | flexDirection: 'column', 24 | backgroundColor: '#24997e', 25 | flex: '1 1 auto', 26 | color: '#000', 27 | alignItems: 'center', 28 | fontSize: '18px', 29 | overflow: 'auto', 30 | padding: '20px 40px' 31 | }, 32 | text: { 33 | paddingLeft: '14px' 34 | }, 35 | imgWrapper: { 36 | margin: '10px 16px' 37 | }, 38 | img: { 39 | width: '100%' 40 | } 41 | } 42 | return ( 43 |
44 |

FAQ

45 | 50 |
51 |
52 | You must use the emulator which comes bundled with Net64+! 53 |
54 |

Emulator settings

55 |
56 | Make sure your emulator settings match the green circles 57 |
58 |
59 | 60 |
61 |
62 | After setting your memory to 16MB, you will have to restart Project64 63 |
64 |
65 | 66 |
67 |

Server Hosting

68 |
69 | You can host your own server by visiting the hosting page.
70 | Joining via LAN can be done by using the LAN IP address which looks like 192.X.X.X.
71 | Joining via internet is only possible, if you correctly port forwarded. 72 | Port forwarding must be done at your router's web interface.
73 |
74 |

Here is a short summary:

75 |
    76 |
  • open a terminal (Win + "cmd" + enter)
  • 77 |
  • type "ipconfig"
  • 78 |
  • 79 | find the entry from your Ethernet or Wifi adapter showing your default gateway, 80 | which should look like 192.X.X.1/0 81 |
  • 82 |
  • open a browser and type in this address
  • 83 |
  • this is your router's web interface. You will have to log in with your credentials
  • 84 |
  • 85 | the next steps heavily depend on the router you are using. 86 | You will have to find an entry about port forwarding and 87 | set it up for the respective port that you want to use 88 |
  • 89 |
90 |
91 | If you want your server to be publicly visibile, you will have to get an API key from 92 | SMMDB profile page. 93 | Never share your API key with anyone! 94 |
95 |
96 | ) 97 | } 98 | } 99 | export const FaqView = connect()(View) 100 | -------------------------------------------------------------------------------- /src/renderer/components/views/HostView.scss: -------------------------------------------------------------------------------- 1 | .host-view { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: #24997e; 5 | flex: 1 1 auto; 6 | overflow-y: auto; 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/views/HostView.tsx: -------------------------------------------------------------------------------- 1 | import './HostView.scss' 2 | 3 | import * as React from 'react' 4 | import { connect, Dispatch } from 'react-redux' 5 | 6 | import { ConnectionArea } from '../areas/ConnectionArea' 7 | import { HostArea } from '../areas/HostArea' 8 | import { State } from '../../../models/State.model' 9 | import { Server } from '../../../models/Server.model' 10 | 11 | interface ConnectViewProps { 12 | dispatch: Dispatch 13 | server: Server 14 | connectionError: string 15 | } 16 | 17 | class View extends React.PureComponent { 18 | public render (): JSX.Element { 19 | const server = this.props.server 20 | return ( 21 |
22 | 23 | { 24 | server && 25 | 26 | } 27 |
28 | ) 29 | } 30 | } 31 | export const HostView = connect((state: State) => ({ 32 | server: state.connection.server 33 | }))(View) 34 | -------------------------------------------------------------------------------- /src/renderer/components/views/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect, Dispatch } from 'react-redux' 3 | 4 | import { SMMButton } from '../buttons/SMMButton' 5 | import { State } from '../../../models/State.model' 6 | 7 | interface MainViewProps { 8 | dispatch: Dispatch 9 | } 10 | 11 | class View extends React.PureComponent { 12 | constructor (public props: MainViewProps) { 13 | super(props) 14 | } 15 | 16 | public render (): JSX.Element { 17 | const styles: Record = { 18 | main: { 19 | display: 'flex', 20 | flexDirection: 'column', 21 | backgroundColor: '#24997e', 22 | flex: '1 1 auto', 23 | color: '#000', 24 | alignItems: 'center', 25 | fontSize: '18px', 26 | overflow: 'auto', 27 | padding: '20px 40px' 28 | } 29 | } 30 | return ( 31 |
32 | 42 |

Thank you for downloading Net64+

43 |
44 | Net64 aka SM64O allows playing Super Mario 64 in an online multiplayer mode. 45 | Net64+ is the official continuation of the program and features an integrated server list. 46 | You can also play with your friends by hosting your own server with the server software provided. 47 |
48 |

Join our community

49 |
50 | 55 | 60 |
61 |
62 | ) 63 | } 64 | } 65 | export const MainView = connect()(View) 66 | -------------------------------------------------------------------------------- /src/renderer/components/views/SettingsView.scss: -------------------------------------------------------------------------------- 1 | .settings-view { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | flex: 1 0 auto; 6 | padding: 40px; 7 | background-color: #24997e; 8 | font-size: 18px; 9 | color: #000; 10 | height: 0%; 11 | 12 | &-content { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | flex: 1 1 auto; 17 | overflow: auto; 18 | max-height: 100%; 19 | 20 | > * { 21 | margin: 10px 0; 22 | flex: 0 0 auto; 23 | } 24 | } 25 | 26 | &-save { 27 | flex: 0 0 auto; 28 | } 29 | 30 | &-setting { 31 | width: 100%; 32 | display: flex; 33 | } 34 | 35 | &-hotkeys { 36 | display: flex; 37 | flex-wrap: wrap; 38 | align-items: center; 39 | justify-content: space-around; 40 | 41 | > * { 42 | margin: 2px 4px; 43 | } 44 | } 45 | 46 | &-sortable-list { 47 | display: flex; 48 | flex-direction: column; 49 | 50 | > * { 51 | margin: 2px 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/global.scss' 2 | 3 | import * as React from 'react' 4 | import { render } from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import { Route } from 'react-router-dom' 7 | import { Store } from 'redux' 8 | import { ConnectedRouter } from 'react-router-redux' 9 | import * as rimraf from 'rimraf' 10 | import { remote } from 'electron' 11 | import { History, createMemoryHistory as createHistory } from 'history' 12 | 13 | import * as fs from 'fs' 14 | import * as path from 'path' 15 | 16 | import { initReducer } from './reducers' 17 | import { Connector } from './Connector' 18 | import { AppView } from './components/views/AppView' 19 | import { State, SaveState, SaveStateDraft, ElectronSaveData } from '../models/State.model' 20 | import { GamepadManager } from './GamepadManager' 21 | 22 | export let store: Store 23 | export const connector = new Connector() 24 | export let gamepadManager: GamepadManager 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 27 | ;(async () => { 28 | const history: History = createHistory() 29 | const save: SaveState = await loadSaveData() 30 | store = initReducer(history, save) 31 | 32 | const saveState = store.getState().save 33 | connector.changeEmuChat(saveState.appSaveData.emuChat) 34 | connector.changeHotkeyBindings({ 35 | hotkeyBindings: saveState.appSaveData.hotkeyBindings, 36 | globalHotkeysEnabled: saveState.appSaveData.globalHotkeysEnabled, 37 | username: saveState.appSaveData.username 38 | }) 39 | connector.changeCharacterCyclingOrder({ characterCyclingOrder: saveState.appSaveData.characterCylingOrder }) 40 | gamepadManager = new GamepadManager(window, connector, saveState.appSaveData.gamepadId) 41 | 42 | render( 43 | 44 | 45 | 46 | 47 | , document.getElementById('root') 48 | ) 49 | })() 50 | 51 | process.on('uncaughtException', (err: Error) => { 52 | console.error(err) 53 | }) 54 | 55 | async function loadSaveData (): Promise> { 56 | const appSavePath = remote.app.getPath('userData') 57 | if (!fs.existsSync(appSavePath)) { 58 | fs.mkdirSync(appSavePath) 59 | } 60 | const save: SaveStateDraft = { 61 | appSavePath 62 | } as any 63 | if (fs.existsSync(path.join(appSavePath, 'save.json'))) { 64 | try { 65 | const appSaveData = JSON.parse(fs.readFileSync(path.join(appSavePath, 'save.json'), { 66 | encoding: 'utf8' 67 | })) 68 | if (appSaveData == null) { 69 | await clearAppData(appSavePath) 70 | } else { 71 | save.appSaveData = appSaveData 72 | } 73 | } catch (err) { 74 | await clearAppData(appSavePath) 75 | } 76 | } 77 | return save 78 | } 79 | 80 | function clearAppData (appSavePath: string): Promise { 81 | return new Promise(resolve => { 82 | rimraf(appSavePath, err => { 83 | if (err) { 84 | console.error(err) 85 | } else { 86 | fs.mkdirSync(appSavePath) 87 | } 88 | resolve() 89 | }) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/middlewares/server-middleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareAPI, Action, Dispatch } from 'redux' 2 | 3 | import { ChildProcess } from 'child_process' 4 | 5 | import { ServerActionType, SetServerProcessAction } from '../actions/models/server.model' 6 | import { addServerMessage, exitServerProcess } from '../actions/server' 7 | 8 | const decoder = new TextDecoder('utf8') 9 | 10 | export const serverMiddleware = ({ dispatch }: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { 11 | const nextAction = next(action) 12 | if (!nextAction) return nextAction 13 | switch (nextAction.type) { 14 | case ServerActionType.SET_SERVER_PROCESS: 15 | const setServerProcessAction = nextAction as SetServerProcessAction 16 | listenToProcessOutput(setServerProcessAction.process, dispatch) 17 | break 18 | } 19 | return nextAction 20 | } 21 | 22 | function listenToProcessOutput (process: ChildProcess, dispatch: Dispatch): void { 23 | process.on('exit', (code: number) => { 24 | if (code != null) dispatch(exitServerProcess(code)) 25 | }) 26 | process.on('error', (err: Error) => { 27 | console.error(err) 28 | }) 29 | process.stdout!.on('data', (chunk: Buffer) => { 30 | dispatch(addServerMessage(decoder.decode(chunk))) 31 | }) 32 | process.stderr!.on('data', (chunk: Buffer) => { 33 | dispatch(addServerMessage(decoder.decode(chunk), true)) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/middlewares/snackbar-middleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareAPI, AnyAction, Dispatch } from 'redux' 2 | 3 | import { SnackbarActionType } from '../actions/models/snackbar.model' 4 | import { hideSnackbar } from '../actions/snackbar' 5 | 6 | let timer: NodeJS.Timer | undefined 7 | 8 | export const snackbarMiddleware = ( 9 | { dispatch }: MiddlewareAPI 10 | ) => (next: Dispatch) => (action: AnyAction) => { 11 | const nextAction = next(action) 12 | if (!nextAction) return nextAction 13 | switch (nextAction.type) { 14 | case SnackbarActionType.SHOW_SNACKBAR: 15 | if (timer) { 16 | clearTimeout(timer) 17 | } 18 | timer = setTimeout(() => { 19 | dispatch(hideSnackbar()) 20 | timer = undefined 21 | }, 4000) 22 | break 23 | } 24 | return nextAction 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/reducers/chat.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import { initialState } from '.' 4 | import { ChatAction } from '../actions/models/chat.model' 5 | import { ChatState, ChatStateDraft } from '../../models/State.model' 6 | 7 | const HISTORY_LENGTH = 100 8 | 9 | export const chat = (state: ChatState = initialState.chat, action: ChatAction) => 10 | produce(state, (draft: ChatStateDraft) => { 11 | switch (action.type) { 12 | case 'ADD_GLOBAL_CHAT_MESSAGE': 13 | draft.global = [...state.global, action.chatMessage] 14 | if (draft.global.length > HISTORY_LENGTH) { 15 | draft.global = draft.global.slice(1) 16 | } 17 | break 18 | case 'CLEAR_GLOBAL_CHAT_MESSAGES': 19 | draft.global = [] 20 | break 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/renderer/reducers/connection.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import { initialState } from '.' 4 | import { ConnectionAction, ConnectionActionType } from '../actions/models/connection.model' 5 | import { ConnectionState, ConnectionStateDraft } from '../../models/State.model' 6 | import { Player } from '../../models/Emulator.model' 7 | 8 | export const connection = (state: ConnectionState = initialState.connection, action: ConnectionAction) => 9 | produce(state, (draft: ConnectionStateDraft) => { 10 | switch (action.type) { 11 | case ConnectionActionType.SET_SERVER: 12 | draft.server = action.server 13 | draft.error = '' 14 | draft.authenticated = true 15 | break 16 | case ConnectionActionType.SET_CONNECTION_ERROR: 17 | draft.error = action.error 18 | break 19 | case ConnectionActionType.SET_PLAYERS: 20 | if (!draft.server) return 21 | const players: Array = new Array(25).fill(null) 22 | for (const player of action.players) { 23 | if (!player.player || player.playerId == null) continue 24 | players[player.playerId] = player.player 25 | } 26 | draft.server.players = players 27 | break 28 | case ConnectionActionType.SET_PLAYER: 29 | if (!draft.server) return 30 | if (!draft.server.players) draft.server.players = [] 31 | draft.server.players[action.playerId] = action.player 32 | break 33 | case ConnectionActionType.SET_PLAYER_ID: 34 | draft.playerId = action.playerId 35 | break 36 | case ConnectionActionType.UPDATE_PLAYER_POSITIONS: 37 | draft.selfPos = action.self 38 | draft.cameraAngle = action.cameraAngle 39 | if (!draft.server) return 40 | if (!draft.server.players) return 41 | for (let i = 0; i < action.positions.length; i++) { 42 | const position = action.positions[i] 43 | if (!position) continue 44 | if (!draft.server.players[i + 1]) continue 45 | const player = draft.server.players[i + 1] as Player 46 | const prevCourse = player.position?.course ?? 0 47 | player.position = position 48 | if (player.position.course === 0) { 49 | player.position.course = prevCourse 50 | } 51 | } 52 | break 53 | case ConnectionActionType.GAME_MODE: 54 | if (!draft.server) return 55 | draft.server.gameMode = action.gameMode 56 | break 57 | case ConnectionActionType.AUTHENTICATION_REQUIRED: 58 | draft.authenticated = false 59 | break 60 | case ConnectionActionType.AUTHENTICATION_ACCEPTED: 61 | draft.authenticated = true 62 | break 63 | case ConnectionActionType.AUTHENTICATION_DENIED: 64 | draft.authenticationThrottle = Date.now() + action.throttle * 1000 65 | break 66 | case ConnectionActionType.DISCONNECT: 67 | draft.server = null 68 | draft.playerId = null 69 | break 70 | } 71 | }) 72 | -------------------------------------------------------------------------------- /src/renderer/reducers/emulator.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import { initialState } from '.' 4 | import { EmulatorAction, EmulatorActionType } from '../actions/models/emulator.model' 5 | import { EmulatorState, EmulatorStateDraft } from '../../models/State.model' 6 | 7 | export const emulator = (state: EmulatorState = initialState.emulator, action: EmulatorAction) => 8 | produce(state, (draft: EmulatorStateDraft) => { 9 | switch (action.type) { 10 | case EmulatorActionType.UPDATE_EMULATORS: 11 | draft.emulators = action.emulators 12 | break 13 | case EmulatorActionType.IS_CONNECTED_TO_EMULATOR: 14 | draft.isConnectedToEmulator = action.isConnectedToEmulator 15 | break 16 | case EmulatorActionType.SET_ERROR: 17 | draft.error = action.error ?? '' 18 | break 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/renderer/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers, Store } from 'redux' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import { History } from 'history' 4 | 5 | import { save } from './save' 6 | import { router } from './router' 7 | import { connection } from './connection' 8 | import { emulator } from './emulator' 9 | import { server } from './server' 10 | import { chat } from './chat' 11 | import { snackbar } from './snackbar' 12 | import { serverMiddleware } from '../middlewares/server-middleware' 13 | import { snackbarMiddleware } from '../middlewares/snackbar-middleware' 14 | import { MIN_LENGTH_USERNAME, MAX_LENGTH_USERNAME } from '../components/views/SettingsView' 15 | import { State, SaveState, ElectronSaveData } from '../../models/State.model' 16 | 17 | export let initialState: State 18 | 19 | const APP_SAVE_DATA: ElectronSaveData = { 20 | apiKey: '', 21 | username: '', 22 | character: 0, 23 | emuChat: false, 24 | globalHotkeysEnabled: false, 25 | hotkeyBindings: { 26 | 0: [], // Mario 27 | 1: [], // Luigi 28 | 2: [], // Yoshi 29 | 3: [], // Wario 30 | 4: [], // Peach 31 | 5: [], // Toad 32 | 6: [], // Waluigi 33 | 7: [], // Rosalina 34 | 8: [], // Sonic 35 | 9: [], // Knuckles 36 | 10: [], // Goomba 37 | 11: [], // Kirby 38 | nextCharacter: [], 39 | previousCharacter: [] 40 | }, 41 | characterCylingOrder: [ 42 | { characterId: 0, on: true }, // Mario 43 | { characterId: 1, on: true }, // Luigi 44 | { characterId: 2, on: true }, // Yoshi 45 | { characterId: 3, on: true }, // Wario 46 | { characterId: 4, on: true }, // Peach 47 | { characterId: 5, on: true }, // Toad 48 | { characterId: 6, on: true }, // Waluigi 49 | { characterId: 7, on: true }, // Rosalina 50 | { characterId: 8, on: true }, // Sonic 51 | { characterId: 9, on: true }, // Knuckles 52 | { characterId: 10, on: true }, // Goomba 53 | { characterId: 11, on: true } // Kirby 54 | ], 55 | gamepadId: undefined, 56 | lastIp: 'smmdb.net', 57 | lastPort: 3678, 58 | version: '', 59 | serverOptions: { 60 | name: 'A Net64+ Server', 61 | description: 'The **best** Net64+ server ever\n\n:unicorn_face:', 62 | gamemode: 1, 63 | enableGamemodeVote: true, 64 | passwordRequired: false, 65 | password: '', 66 | port: 3678, 67 | enableWebHook: false 68 | } 69 | } 70 | 71 | export function initReducer (history: History, electronSave: SaveState): Store { 72 | let appSaveData: ElectronSaveData = Object.assign({}, APP_SAVE_DATA) 73 | try { 74 | if (electronSave.appSaveData) { 75 | appSaveData = Object.assign(appSaveData, JSON.parse(JSON.stringify(electronSave.appSaveData))) 76 | Object.assign(appSaveData.serverOptions, APP_SAVE_DATA.serverOptions, electronSave.appSaveData.serverOptions) 77 | } 78 | } catch (err) { 79 | appSaveData = Object.assign({}, APP_SAVE_DATA) 80 | } 81 | const username = appSaveData.username.replace(/\W/g, '') 82 | if ( 83 | username !== appSaveData.username || 84 | username.length < MIN_LENGTH_USERNAME || 85 | username.length > MAX_LENGTH_USERNAME 86 | ) { 87 | appSaveData = Object.assign({}, APP_SAVE_DATA) 88 | } 89 | const appSavePath: string = electronSave.appSavePath || '' 90 | initialState = { 91 | save: { 92 | appSaveData, 93 | appSavePath 94 | }, 95 | router: { 96 | location: null 97 | }, 98 | connection: { 99 | server: null, 100 | playerId: null, 101 | selfPos: { x: 0, y: 0, rotation: 0, course: 0 }, 102 | cameraAngle: 0, 103 | authenticated: true, 104 | authenticationThrottle: 0, 105 | hasToken: false, 106 | error: '' 107 | }, 108 | emulator: { 109 | emulators: [], 110 | isConnectedToEmulator: false, 111 | error: '' 112 | }, 113 | server: { 114 | process: null, 115 | exitCode: null, 116 | server: null, 117 | messages: [] 118 | }, 119 | chat: { 120 | global: [] 121 | }, 122 | snackbar: { 123 | message: null 124 | } 125 | } 126 | const reducers = { 127 | save, 128 | router, 129 | connection, 130 | emulator, 131 | server, 132 | chat, 133 | snackbar 134 | } 135 | const middleware = applyMiddleware(serverMiddleware as any, snackbarMiddleware as any, routerMiddleware(history)) 136 | return createStore(combineReducers(reducers as any), initialState, middleware) as any 137 | } 138 | -------------------------------------------------------------------------------- /src/renderer/reducers/router.ts: -------------------------------------------------------------------------------- 1 | import { LOCATION_CHANGE } from 'react-router-redux' 2 | import produce from 'immer' 3 | 4 | import { initialState } from '.' 5 | import { RouterAction } from '../actions/models/router.model' 6 | import { RouterState, RouterStateDraft } from '../../models/State.model' 7 | 8 | export const router = (state: RouterState = initialState.router, action: RouterAction) => 9 | produce(state, (draft: RouterStateDraft) => { 10 | switch (action.type) { 11 | case LOCATION_CHANGE: 12 | draft.location = action.payload 13 | break 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/renderer/reducers/save.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | 6 | import { initialState } from '.' 7 | import { SaveAction } from '../actions/models/save.model' 8 | import { SaveState, SaveStateDraft, ElectronSaveDataDraft } from '../../models/State.model' 9 | 10 | export const save = (state: SaveState = initialState.save, action: SaveAction) => { 11 | const nextState = produce(state, (draft: SaveStateDraft) => { 12 | switch (action.type) { 13 | case 'SET_USERNAME': 14 | draft.appSaveData.username = action.username 15 | break 16 | case 'SET_CHARACTER': 17 | draft.appSaveData.character = action.character 18 | break 19 | case 'SET_EMU_CHAT': 20 | draft.appSaveData.emuChat = action.emuChat 21 | break 22 | case 'SET_GLOBAL_HOTKEYS': 23 | draft.appSaveData.globalHotkeysEnabled = action.globalHotkeysEnabled 24 | break 25 | case 'SET_HOTKEY_BINDINGS': 26 | draft.appSaveData.hotkeyBindings = action.hotkeyBindings 27 | break 28 | case 'SET_CHARACTER_CYCLING_ORDER': 29 | draft.appSaveData.characterCylingOrder = action.characterCyclingOrder 30 | break 31 | case 'SET_GAMEPAD_ID': 32 | draft.appSaveData.gamepadId = action.gamepadId 33 | break 34 | case 'ADD_API_KEY': 35 | draft.appSaveData.apiKey = action.apiKey 36 | break 37 | case 'DELETE_API_KEY': 38 | draft.appSaveData.apiKey = '' 39 | break 40 | case 'SET_VERSION': 41 | draft.appSaveData.version = action.version 42 | break 43 | case 'SAVE_SERVER_OPTIONS': 44 | draft.appSaveData.apiKey = action.apiKey 45 | draft.appSaveData.serverOptions = action.serverOptions 46 | break 47 | } 48 | }) 49 | saveState(nextState) 50 | return nextState 51 | } 52 | 53 | function saveState (state: SaveState): void { 54 | fs.writeFile(path.join(state.appSavePath, 'save.json'), JSON.stringify(state.appSaveData, null, 2), () => {}) 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/reducers/server.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import { initialState } from '.' 4 | import { ServerState, ServerStateDraft, IoChannel, ConsoleServerMessage } from '../../models/State.model' 5 | import { ServerActionType, ServerAction } from '../actions/models/server.model' 6 | 7 | export const server = (state: ServerState = initialState.server, action: ServerAction) => 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 9 | // @ts-ignore 10 | produce(state, (draft: ServerStateDraft) => { 11 | switch (action.type) { 12 | case ServerActionType.SET_SERVER_PROCESS: 13 | draft.process = action.process 14 | draft.exitCode = null 15 | break 16 | case ServerActionType.REMOVE_SERVER_PROCESS: 17 | try { 18 | if (draft.process && !draft.process.killed) draft.process.kill('SIGINT') 19 | } catch (err) { 20 | // process already closed 21 | } 22 | draft.process = null 23 | draft.exitCode = null 24 | draft.messages = [] 25 | break 26 | case ServerActionType.EXIT_SERVER_PROCESS: 27 | draft.process = null 28 | draft.exitCode = action.code 29 | break 30 | case ServerActionType.ADD_SERVER_MESSAGE: 31 | const messages = action.message.split('\n') 32 | const chatMessages: ConsoleServerMessage[] = [] 33 | for (const message of messages) { 34 | if (!message) continue 35 | const isWarn = action.isStdErr 36 | const isErr = isWarn && message.startsWith('ERROR') 37 | chatMessages.push({ 38 | key: Math.random().toString(36).substr(0, 6), 39 | message, 40 | channel: isErr ? IoChannel.Err : isWarn ? IoChannel.Warn : IoChannel.Out 41 | }) 42 | } 43 | draft.messages = [ 44 | ...draft.messages, 45 | ...chatMessages 46 | ] 47 | break 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /src/renderer/reducers/snackbar.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | import { initialState } from '.' 4 | import { SnackbarStateDraft, SnackbarState } from '../../models/State.model' 5 | import { SnackbarActionType, SnackbarAction } from '../actions/models/snackbar.model' 6 | 7 | export const snackbar = (state: SnackbarState = initialState.snackbar, action: SnackbarAction) => 8 | produce(state, (draft: SnackbarStateDraft) => { 9 | switch (action.type) { 10 | case SnackbarActionType.SHOW_SNACKBAR: 11 | draft.message = action.message 12 | break 13 | case SnackbarActionType.HIDE_SNACKBAR: 14 | draft.message = null 15 | break 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /src/renderer/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 58 | 59 | 60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /src/renderer/utils/chat.util.ts: -------------------------------------------------------------------------------- 1 | import * as marked from 'marked' 2 | import { emojify } from 'node-emoji' 3 | import { sanitize } from 'dompurify' 4 | 5 | import { store } from '..' 6 | import { MAX_LENGTH_USERNAME } from '../components/views/SettingsView' 7 | import { addGlobalChatMessage, clearGlobalChatMessages } from '../actions/chat' 8 | import { ChatMessage } from '../../models/State.model' 9 | 10 | export function addGlobalMessage (message: string, username: string, isTrusted = false) { 11 | const date = new Date() 12 | const sanitizedMessage = isTrusted 13 | ? sanitize(emojify(marked(message))) 14 | : emojify(message) 15 | const chatMessage: ChatMessage = { 16 | key: date.getUTCMilliseconds(), 17 | time: `\ 18 | ${String(date.getHours()).padStart(2, '00')}\ 19 | :${String(date.getMinutes()).padStart(2, '00')}\ 20 | :${String(date.getSeconds()).padStart(2, '00')}`, 21 | message: sanitizedMessage, 22 | username: sanitize(username).substr(0, MAX_LENGTH_USERNAME), 23 | isTrusted 24 | } 25 | store.dispatch(addGlobalChatMessage(chatMessage)) 26 | } 27 | 28 | export function clearGlobalMessages (): void { 29 | store.dispatch(clearGlobalChatMessages()) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/utils/helper.util.ts: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | import { extract } from 'tar' 3 | 4 | import * as fs from 'fs' 5 | import * as path from 'path' 6 | import { promisify } from 'util' 7 | 8 | import { Release } from '../../models/Release.model' 9 | 10 | const mkdir = promisify(fs.mkdir) 11 | const readdir = promisify(fs.readdir) 12 | const writeFile = promisify(fs.writeFile) 13 | 14 | export function getCurrentServerVersion (): string | undefined { 15 | const serverPath = getServerPath() 16 | let currentVersion: string | undefined 17 | for (const serverPathVersion of fs.readdirSync(serverPath)) { 18 | const serverVersionTag = serverPathVersion.replace(serverPath, '').slice(0, -1) 19 | if (isVersionNewer(serverVersionTag, currentVersion)) continue 20 | currentVersion = serverPathVersion 21 | } 22 | if (currentVersion) saveAndExtractServer(currentVersion) 23 | return currentVersion 24 | } 25 | 26 | export async function saveAndExtractServer (version: string, buffer?: ArrayBuffer): Promise { 27 | const serverPath = getServerPath() 28 | const serverVersionPath = path.join(serverPath, version) 29 | try { 30 | await mkdir(serverVersionPath) 31 | } catch (err) {} 32 | const gzFile = path.join(serverVersionPath, 'net64plus-server.tar.gz') 33 | if (buffer) await writeFile(gzFile, Buffer.from(buffer)) 34 | let files = await readdir(serverVersionPath) 35 | for (const file of files) { 36 | if (file.includes('net64plus-server') && !file.includes('tar.gz')) { 37 | return path.join(serverVersionPath, file) 38 | } 39 | } 40 | await extract({ 41 | cwd: serverVersionPath, 42 | file: gzFile, 43 | newer: true 44 | }) 45 | files = await readdir(serverVersionPath) 46 | for (const file of files) { 47 | if (file.includes('net64plus-server') && !file.includes('tar.gz')) { 48 | return path.join(serverVersionPath, file) 49 | } 50 | } 51 | return '' 52 | } 53 | 54 | function getServerPath (): string { 55 | const serverPath = path.join(remote.app.getPath('userData'), 'server') 56 | if (!fs.existsSync(serverPath)) { 57 | fs.mkdirSync(serverPath) 58 | } 59 | return serverPath 60 | } 61 | 62 | export function isReleaseValid (release: Release): boolean { 63 | if (release.draft == null || release.draft) return false 64 | if (release.prerelease == null || release.prerelease) return false 65 | if (release.assets == null || release.assets.length === 0) return false 66 | if (!release.tag_name) return false 67 | return true 68 | } 69 | 70 | export function isVersionNewer (versionTag: string, currentVersionTag?: string): boolean { 71 | let [major, minor, patch] = versionTag.split('.') 72 | .map(mapVersionToNumber) 73 | let [currentMajor, currentMinor, currentPatch] = currentVersionTag 74 | ? currentVersionTag.split('.') 75 | .map(mapVersionToNumber) 76 | : [0, 0, 0] 77 | if (patch == null) patch = 0 78 | if (currentPatch == null) currentPatch = 0 79 | const versionValue = major * 10000 + minor * 100 + patch 80 | const currentVersionValue = currentMajor * 10000 + currentMinor * 100 + currentPatch 81 | return versionValue > currentVersionValue 82 | } 83 | 84 | function mapVersionToNumber (versionNumber: string): number { 85 | return versionNumber != null ? parseInt(versionNumber) : 0 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | .global-hidden { 2 | display: none !important; 3 | } 4 | 5 | .global-invisible { 6 | visibility: hidden; 7 | } 8 | 9 | .markdown img { 10 | max-width: 100%; 11 | max-height: 200px; 12 | } 13 | .markdown a { 14 | color: #0001A7; 15 | cursor: pointer; 16 | } 17 | .chat p { 18 | margin: 0 0 0 12px; 19 | } 20 | .chat p.header { 21 | margin: 0; 22 | display: inline-block; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/Buffer.util.ts: -------------------------------------------------------------------------------- 1 | export function buf2hex (buffer: Uint8Array): string { 2 | return Array.prototype.map.call(buffer, (x: any) => ('00' + x.toString(16)).slice(-2)).join(' ') 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "jsx": "react", 7 | "sourceMap": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | }, 11 | "include": [ 12 | "./src/**/*" 13 | ], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(env) { 2 | return require(`./webpack.${env}.js`) 3 | } -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 6 | 7 | const getCompatMin = require('./compat-list') 8 | 9 | const [ major, minor, patch ] = process.env.npm_package_compatVersion.split('.') 10 | const [ packageMajor, packageMinor, packagePatch ] = process.env.npm_package_version.split('.') 11 | const [ compatMinMajor, compatMinMinor ] = getCompatMin(process.env.npm_package_version) 12 | 13 | module.exports = [ 14 | { 15 | target: 'electron-renderer', 16 | mode: 'development', 17 | entry: { 18 | renderer: path.join(__dirname, 'src/renderer/index.tsx') 19 | }, 20 | output: { 21 | filename: 'renderer.js', 22 | path: path.join(__dirname, 'build') 23 | }, 24 | devtool: 'source-map', 25 | node: { 26 | __dirname: true, 27 | __filename: false, 28 | console: true, 29 | fs: 'empty', 30 | net: 'empty', 31 | tls: 'empty' 32 | }, 33 | plugins: [ 34 | new webpack.EnvironmentPlugin({ 35 | NODE_ENV: 'development', 36 | VERSION: process.env.npm_package_version.slice(-2) === '.0' ? process.env.npm_package_version.slice(0, process.env.npm_package_version.length - 2) : process.env.npm_package_version, 37 | MAJOR: major, 38 | MINOR: minor, 39 | PATCH: patch, 40 | PACKAGE_MAJOR: packageMajor, 41 | PACKAGE_MINOR: packageMinor, 42 | PACKAGE_PATCH: packagePatch, 43 | COMPAT_MIN_MAJOR: compatMinMajor, 44 | COMPAT_MIN_MINOR: compatMinMinor 45 | }), 46 | new HtmlWebpackPlugin({ 47 | filename: 'index.html', 48 | template: 'src/renderer/template.html' 49 | }), 50 | new webpack.optimize.ModuleConcatenationPlugin(), 51 | new MiniCssExtractPlugin({ 52 | filename: 'styles/[name].[contenthash].css' 53 | }), 54 | ], 55 | resolve: { 56 | extensions: [ '.ts', '.tsx', '.js', '.jsx', '.json' ] 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.tsx?$/, 62 | exclude: /node_modules/, 63 | loader: 'babel-loader' 64 | }, 65 | { 66 | test: /\.(png|jpg)$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 25000, 70 | prefix: path.join(__dirname, 'build') 71 | } 72 | }, 73 | { 74 | test: /\.(woff|ttf)$/, 75 | loader: 'url-loader', 76 | options: { 77 | limit: 25000, 78 | prefix: path.join(__dirname, 'build') 79 | } 80 | }, 81 | { 82 | test: /\.scss$/, 83 | use: [ 84 | MiniCssExtractPlugin.loader, 85 | { loader: 'css-loader', options: { sourceMap: true, importLoaders: 1 } }, 86 | { loader: 'sass-loader', options: { sourceMap: true } }, 87 | ] 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | target: 'electron-main', 94 | mode: 'development', 95 | entry: path.join(__dirname, 'src/main/index.ts'), 96 | output: { 97 | filename: 'index.js', 98 | path: path.join(__dirname, 'build') 99 | }, 100 | devtool: 'inline-source-map', 101 | node: { 102 | __dirname: false, 103 | __filename: false 104 | }, 105 | plugins: [ 106 | new webpack.EnvironmentPlugin({ 107 | NODE_ENV: 'development', 108 | VERSION: process.env.npm_package_version.slice(-2) === '.0' ? process.env.npm_package_version.slice(0, process.env.npm_package_version.length - 2) : process.env.npm_package_version, 109 | MAJOR: major, 110 | MINOR: minor, 111 | PATCH: patch, 112 | PACKAGE_MAJOR: packageMajor, 113 | PACKAGE_MINOR: packageMinor, 114 | PACKAGE_PATCH: packagePatch, 115 | COMPAT_MIN_MAJOR: compatMinMajor, 116 | COMPAT_MIN_MINOR: compatMinMinor 117 | }) 118 | ], 119 | externals: { 120 | winprocess: 'require(require("path").resolve(__dirname, "winprocess"))', 121 | }, 122 | resolve: { 123 | extensions: [ '.ts', '.js', '.json' ] 124 | }, 125 | module: { 126 | rules: [ 127 | { 128 | test: /\.tsx?$/, 129 | exclude: /node_modules/, 130 | loader: 'babel-loader' 131 | } 132 | ] 133 | } 134 | } 135 | ] 136 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 6 | 7 | const getCompatMin = require('./compat-list') 8 | 9 | const [ major, minor, patch ] = process.env.npm_package_compatVersion.split('.') 10 | const [ packageMajor, packageMinor, packagePatch ] = process.env.npm_package_version.split('.') 11 | const [ compatMinMajor, compatMinMinor ] = getCompatMin(process.env.npm_package_version) 12 | 13 | module.exports = [ 14 | { 15 | target: 'electron-renderer', 16 | mode: 'production', 17 | entry: { 18 | renderer: path.join(__dirname, 'src/renderer/index.tsx') 19 | }, 20 | output: { 21 | filename: 'renderer.js', 22 | path: path.join(__dirname, 'build') 23 | }, 24 | node: { 25 | __dirname: true, 26 | __filename: false, 27 | console: true, 28 | fs: 'empty', 29 | net: 'empty', 30 | tls: 'empty' 31 | }, 32 | plugins: [ 33 | new webpack.EnvironmentPlugin({ 34 | NODE_ENV: 'production', 35 | VERSION: process.env.npm_package_version.slice(-2) === '.0' ? process.env.npm_package_version.slice(0, process.env.npm_package_version.length - 2) : process.env.npm_package_version, 36 | MAJOR: major, 37 | MINOR: minor, 38 | PATCH: patch, 39 | PACKAGE_MAJOR: packageMajor, 40 | PACKAGE_MINOR: packageMinor, 41 | PACKAGE_PATCH: packagePatch, 42 | COMPAT_MIN_MAJOR: compatMinMajor, 43 | COMPAT_MIN_MINOR: compatMinMinor 44 | }), 45 | new HtmlWebpackPlugin({ 46 | filename: 'index.html', 47 | template: 'src/renderer/template.html' 48 | }), 49 | new webpack.optimize.ModuleConcatenationPlugin(), 50 | new MiniCssExtractPlugin({ 51 | filename: 'styles/[name].[contenthash].css' 52 | }), 53 | ], 54 | resolve: { 55 | extensions: [ '.ts', '.tsx', '.js', '.jsx', '.json' ] 56 | }, 57 | module: { 58 | rules: [ 59 | { 60 | test: /\.tsx?$/, 61 | exclude: /node_modules/, 62 | use: { 63 | loader: 'babel-loader', 64 | // options: { 65 | // presets: ['minify'] 66 | // } 67 | } 68 | }, 69 | { 70 | test: /\.(png|jpg)$/, 71 | loader: 'url-loader', 72 | options: { 73 | limit: 25000, 74 | prefix: path.join(__dirname, 'build') 75 | } 76 | }, 77 | { 78 | test: /\.(woff|ttf)$/, 79 | loader: 'url-loader', 80 | options: { 81 | limit: 25000, 82 | prefix: path.join(__dirname, 'build') 83 | } 84 | }, 85 | { 86 | test: /\.scss$/, 87 | use: [ 88 | MiniCssExtractPlugin.loader, 89 | { loader: 'css-loader', options: { sourceMap: true, importLoaders: 1 } }, 90 | { loader: 'sass-loader', options: { sourceMap: true } }, 91 | ] 92 | } 93 | ] 94 | } 95 | }, 96 | { 97 | target: 'electron-main', 98 | mode: 'production', 99 | entry: path.join(__dirname, 'src/main/index.ts'), 100 | output: { 101 | filename: 'index.js', 102 | path: path.join(__dirname, 'build') 103 | }, 104 | node: { 105 | __dirname: false, 106 | __filename: false 107 | }, 108 | plugins: [ 109 | new webpack.EnvironmentPlugin({ 110 | NODE_ENV: 'production', 111 | VERSION: process.env.npm_package_version.slice(-2) === '.0' ? process.env.npm_package_version.slice(0, process.env.npm_package_version.length - 2) : process.env.npm_package_version, 112 | MAJOR: major, 113 | MINOR: minor, 114 | PATCH: patch, 115 | PACKAGE_MAJOR: packageMajor, 116 | PACKAGE_MINOR: packageMinor, 117 | PACKAGE_PATCH: packagePatch, 118 | COMPAT_MIN_MAJOR: compatMinMajor, 119 | COMPAT_MIN_MINOR: compatMinMinor 120 | }), 121 | new webpack.optimize.ModuleConcatenationPlugin(), 122 | ], 123 | externals: { 124 | winprocess: 'require(require("path").resolve(__dirname, "winprocess"))', 125 | }, 126 | resolve: { 127 | extensions: [ '.ts', '.js', '.json' ] 128 | }, 129 | module: { 130 | rules: [ 131 | { 132 | test: /\.tsx?$/, 133 | exclude: /node_modules/, 134 | loader: 'babel-loader' 135 | } 136 | ] 137 | } 138 | } 139 | ] 140 | --------------------------------------------------------------------------------