├── .editorconfig ├── .github └── workflows │ ├── build_and_test.yml │ └── deploy_to_azure.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Dockerfile ├── GMCP.md ├── LICENSE ├── LINKS.txt ├── README.md ├── VERSION_HISTORY.md ├── backend ├── .eslintrc.json ├── Explain_Code_On_German.txt ├── jest.config.cjs ├── package-lock.json ├── package.json ├── src │ ├── cleanup.ts │ ├── core │ │ ├── environment │ │ │ ├── environment.spec.ts │ │ │ ├── environment.ts │ │ │ ├── types │ │ │ │ ├── environment-keys.ts │ │ │ │ └── environment.ts │ │ │ └── utils │ │ │ │ ├── get-environment-variable.spec.ts │ │ │ │ ├── get-environment-variable.ts │ │ │ │ └── resolve-modulepath.ts │ │ ├── middleware │ │ │ ├── use-body-parser.ts │ │ │ ├── use-cookie-session.ts │ │ │ ├── use-rest-endpoints.ts │ │ │ ├── use-sockets.ts │ │ │ └── use-static-files.ts │ │ ├── routes │ │ │ └── routes.ts │ │ └── sockets │ │ │ ├── socket-manager.ts │ │ │ └── types │ │ │ ├── client-to-server-events.ts │ │ │ ├── inter-server-events.ts │ │ │ ├── mud-connections.ts │ │ │ └── server-to-client-events.ts │ ├── features │ │ ├── auth │ │ │ ├── auth-routes.ts │ │ │ └── auth.ts │ │ ├── telnet │ │ │ ├── README.md │ │ │ ├── telnet-client.ts │ │ │ ├── types │ │ │ │ ├── telnet-control-sequences.ts │ │ │ │ ├── telnet-negotiation-result.ts │ │ │ │ ├── telnet-negotiations.ts │ │ │ │ ├── telnet-option-handler.ts │ │ │ │ ├── telnet-options.ts │ │ │ │ └── telnet-subnegotiation-result.ts │ │ │ └── utils │ │ │ │ ├── handle-charset-option.ts │ │ │ │ ├── handle-echo-option.ts │ │ │ │ ├── handle-naws-option.ts │ │ │ │ ├── log-negotiation.ts │ │ │ │ └── telnet-socket-wrapper.ts │ │ └── websockets │ │ │ └── socket-manager.ts.bak │ ├── main.ts │ └── shared │ │ └── utils │ │ ├── create-http-server.ts │ │ ├── is-buffer-encoding.spec.ts │ │ ├── is-buffer-encoding.ts │ │ ├── logger.ts │ │ ├── size-to-buffer.spec.ts │ │ ├── size-to-buffer.ts │ │ ├── supported-encodings.ts │ │ ├── txt-to-buffer.ts │ │ ├── val16-to-buffer.spec.ts │ │ └── val16-to-buffer.ts └── tsconfig.json ├── dockerfiles ├── README.md ├── ng_unitopia_test.dockerfile ├── w3_docker_compose.yml ├── w3_docker_compose_local.yml ├── w3_docker_compose_sb.yml ├── w3_docker_compose_secret.yml ├── w3_docker_compose_test.yml ├── w3_docker_compose_test_neu.yml └── w3_docker_compose_with_apache.yml ├── frontend ├── .eslintrc.js ├── .gitignore ├── angular.json ├── jest.config.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── setup-jest.ts ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── menu.module.ts │ │ │ │ ├── menu.service.ts │ │ │ │ ├── mud-menu │ │ │ │ │ ├── mud-menu.component.html │ │ │ │ │ ├── mud-menu.component.scss │ │ │ │ │ └── mud-menu.component.ts │ │ │ │ └── types │ │ │ │ │ └── menu-state.ts │ │ │ └── mud │ │ │ │ ├── components │ │ │ │ ├── mud-input │ │ │ │ │ ├── mud-input.component.html │ │ │ │ │ ├── mud-input.component.scss │ │ │ │ │ └── mud-input.component.ts │ │ │ │ ├── mud-output │ │ │ │ │ ├── mud-output.component.html │ │ │ │ │ ├── mud-output.component.scss │ │ │ │ │ └── mud-output.component.ts │ │ │ │ └── mud-span │ │ │ │ │ ├── mud-span.component.html │ │ │ │ │ ├── mud-span.component.scss │ │ │ │ │ └── mud-span.component.ts │ │ │ │ ├── mud-client │ │ │ │ ├── mud-client.component.html │ │ │ │ ├── mud-client.component.scss │ │ │ │ └── mud-client.component.ts │ │ │ │ ├── mud.module.ts │ │ │ │ ├── mud.service.ts │ │ │ │ ├── types │ │ │ │ ├── mud-message.ts │ │ │ │ └── mud-signal-handler-data.ts │ │ │ │ └── utils │ │ │ │ ├── do-focus.ts │ │ │ │ ├── keyboard-handler.ts │ │ │ │ ├── mud-process-data.ts │ │ │ │ ├── mud-process-signals.ts │ │ │ │ ├── scroll.ts │ │ │ │ └── table-output.ts │ │ ├── features │ │ │ ├── ansi │ │ │ │ ├── index.ts │ │ │ │ ├── models │ │ │ │ │ ├── ansi256-colors.ts │ │ │ │ │ ├── default-formatting-data.ts │ │ │ │ │ └── escape-sequences.ts │ │ │ │ ├── types │ │ │ │ │ ├── ansi-data.ts │ │ │ │ │ └── format-data.ts │ │ │ │ └── utils │ │ │ │ │ ├── apply-ansi-attributes.spec.ts │ │ │ │ │ ├── apply-ansi-attributes.ts │ │ │ │ │ ├── colors │ │ │ │ │ ├── convert-rgb-to-hex.spec.ts │ │ │ │ │ ├── convert-rgb-to-hex.ts │ │ │ │ │ ├── extract-colors.spec.ts │ │ │ │ │ ├── extract-colors.ts │ │ │ │ │ ├── inv-color.spec.ts │ │ │ │ │ ├── inv-color.ts │ │ │ │ │ ├── invert-grayscale.spec.ts │ │ │ │ │ └── invert-grayscale.ts │ │ │ │ │ ├── converter │ │ │ │ │ ├── decode-binary-base64.spec.ts │ │ │ │ │ ├── decode-binary-base64.ts │ │ │ │ │ ├── encode-binary-base64.spec.ts │ │ │ │ │ ├── encode-binary-base64.ts │ │ │ │ │ ├── format-to-hex.spec.ts │ │ │ │ │ └── format-to-hex.ts │ │ │ │ │ ├── process-ansi-codes.spec.ts │ │ │ │ │ ├── process-ansi-codes.ts │ │ │ │ │ ├── process-ansi-data.spec.ts │ │ │ │ │ ├── process-ansi-data.ts │ │ │ │ │ ├── process-ansi-sequences.spec.ts │ │ │ │ │ └── process-ansi-sequences.ts │ │ │ ├── config │ │ │ │ ├── index.ts │ │ │ │ ├── models │ │ │ │ │ ├── default-webmud-config.ts │ │ │ │ │ └── mud_config.json │ │ │ │ ├── mud-config.service.ts │ │ │ │ └── types │ │ │ │ │ ├── mud-config.ts │ │ │ │ │ └── webmud-config.ts │ │ │ ├── files │ │ │ │ └── files.service.ts │ │ │ ├── gmcp │ │ │ │ ├── gmcp-config.ts │ │ │ │ ├── gmcp-menu.ts │ │ │ │ ├── gmcp.module.ts │ │ │ │ ├── gmcp.service.ts │ │ │ │ └── index.ts │ │ │ ├── modeless │ │ │ │ ├── box.ts │ │ │ │ ├── char-stat │ │ │ │ │ ├── char-stat.component.html │ │ │ │ │ ├── char-stat.component.scss │ │ │ │ │ └── char-stat.component.ts │ │ │ │ ├── dirlist │ │ │ │ │ ├── dirlist.component.html │ │ │ │ │ ├── dirlist.component.scss │ │ │ │ │ └── dirlist.component.ts │ │ │ │ ├── editor │ │ │ │ │ ├── editor.component.html │ │ │ │ │ ├── editor.component.scss │ │ │ │ │ └── editor.component.ts │ │ │ │ ├── flexible-area │ │ │ │ │ ├── flexible-area.component.html │ │ │ │ │ ├── flexible-area.component.scss │ │ │ │ │ └── flexible-area.component.ts │ │ │ │ ├── index.ts │ │ │ │ ├── keyone │ │ │ │ │ ├── keyone.component.html │ │ │ │ │ ├── keyone.component.scss │ │ │ │ │ └── keyone.component.ts │ │ │ │ ├── keypad-config │ │ │ │ │ ├── keypad-config.component.html │ │ │ │ │ ├── keypad-config.component.scss │ │ │ │ │ └── keypad-config.component.ts │ │ │ │ ├── keypad │ │ │ │ │ ├── keypad.component.html │ │ │ │ │ ├── keypad.component.scss │ │ │ │ │ └── keypad.component.ts │ │ │ │ ├── modeless.module.ts │ │ │ │ ├── resizable-draggable │ │ │ │ │ ├── resizable-draggable.component.html │ │ │ │ │ ├── resizable-draggable.component.scss │ │ │ │ │ └── resizable-draggable.component.ts │ │ │ │ └── window │ │ │ │ │ ├── window.component.html │ │ │ │ │ ├── window.component.scss │ │ │ │ │ └── window.component.ts │ │ │ ├── mudconfig │ │ │ │ ├── index.ts │ │ │ │ ├── mud-config.ts │ │ │ │ ├── mudconfig.module.ts │ │ │ │ └── unitopia.service.ts │ │ │ ├── settings │ │ │ │ ├── color-settings │ │ │ │ │ ├── color-settings.component.html │ │ │ │ │ ├── color-settings.component.scss │ │ │ │ │ └── color-settings.component.ts │ │ │ │ ├── editor-search │ │ │ │ │ ├── editor-search.component.html │ │ │ │ │ ├── editor-search.component.scss │ │ │ │ │ └── editor-search.component.ts │ │ │ │ ├── index.ts │ │ │ │ └── settings.module.ts │ │ │ ├── sockets │ │ │ │ ├── index.ts │ │ │ │ ├── sockets.service.ts │ │ │ │ └── types │ │ │ │ │ ├── client-to-server-events.ts │ │ │ │ │ └── server-to-client-events.ts │ │ │ └── widgets │ │ │ │ ├── index.ts │ │ │ │ ├── inventory │ │ │ │ ├── inventory.component.html │ │ │ │ ├── inventory.component.scss │ │ │ │ └── inventory.component.ts │ │ │ │ └── widgets.module.ts │ │ └── shared │ │ │ ├── WINDOW_PROVIDERS.ts │ │ │ ├── char-data.spec.ts │ │ │ ├── char-data.ts │ │ │ ├── color-settings.ts │ │ │ ├── file-info.ts │ │ │ ├── index.ts │ │ │ ├── inventory-list.ts │ │ │ ├── keypad-data.ts │ │ │ ├── mud-list-item.ts │ │ │ ├── mud-signals.ts │ │ │ ├── prime.module.ts │ │ │ ├── server-config.service.ts │ │ │ ├── types │ │ │ ├── secure-string.ts │ │ │ └── with-required.ts │ │ │ ├── utils │ │ │ ├── word-wrap.spec.ts │ │ │ └── word-wrap.ts │ │ │ ├── window-config.ts │ │ │ └── window.service.ts │ ├── assets │ │ ├── .gitkeep │ │ └── icons │ │ │ ├── icon-128x128.png │ │ │ ├── icon-144x144.png │ │ │ ├── icon-152x152.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-384x384.png │ │ │ ├── icon-512x512.png │ │ │ ├── icon-72x72.png │ │ │ ├── icon-96x96.png │ │ │ ├── sb-icon-128x128.png │ │ │ ├── sb-icon-144x144.png │ │ │ ├── sb-icon-152x152.png │ │ │ ├── sb-icon-192x192.png │ │ │ ├── sb-icon-384x384.png │ │ │ ├── sb-icon-48x48.png │ │ │ ├── sb-icon-512x512.png │ │ │ ├── sb-icon-72x72.png │ │ │ ├── sb-icon-96x96.png │ │ │ ├── unitopia-icon-128x128.png │ │ │ ├── unitopia-icon-144x144.png │ │ │ ├── unitopia-icon-152x152.png │ │ │ ├── unitopia-icon-192x192.png │ │ │ ├── unitopia-icon-384x384.png │ │ │ ├── unitopia-icon-48x48.png │ │ │ ├── unitopia-icon-512x512.png │ │ │ ├── unitopia-icon-72x72.png │ │ │ └── unitopia-icon-96x96.png │ ├── environments │ │ ├── environment.interface.ts │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── manifest.webmanifest │ └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | branches: [develop] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20.12.2] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: "npm" 22 | - run: npm ci 23 | - run: npm run build 24 | - run: npm run test 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_azure.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy to azure 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | env: 10 | AZURE_WEBAPP_NAME: unitopia-client 11 | AZURE_WEBAPP_PACKAGE_PATH: "backend/dist" 12 | NODE_VERSION: "20.12.2" 13 | 14 | jobs: 15 | deploy: 16 | environment: 17 | name: "azure" 18 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ env.NODE_VERSION }} 27 | cache: "npm" 28 | 29 | - name: Install all dependencies and build everything 30 | run: | 31 | npm ci 32 | npm run build:prod --if-present 33 | 34 | - name: Install raw dependencies for backend 35 | run: | 36 | cd ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} 37 | npm install --omit=dev 38 | 39 | - name: "Deploy to Azure Web App" 40 | id: deploy-to-webapp 41 | uses: azure/webapps-deploy@v3 42 | with: 43 | app-name: ${{ env.AZURE_WEBAPP_NAME }} 44 | slot-name: "production" 45 | publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} 46 | package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} 47 | restart: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignores all .nx files - generated due a bug in dependencies - see https://github.com/nrwl/nx-console/issues/1975 2 | .nx 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | dist/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | # gnomi.txt 68 | Gnomi.txt 69 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "angular.ng-template", 4 | "dbaeumer.vscode-eslint", 5 | "firsttris.vscode-jest-runner", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "[global] debug npm run start", 6 | "command": "npm run start", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | }, 10 | { 11 | "name": "[frontend] Launch Chrome Debugger", 12 | "request": "launch", 13 | "type": "chrome", 14 | "url": "http://localhost:4200", 15 | "webRoot": "${workspaceFolder}/frontend" 16 | }, 17 | { 18 | "name": "[backend] Attach node debugger", 19 | "type": "node", 20 | "request": "attach", 21 | "processId": "${command:PickProcess}" 22 | }, 23 | { 24 | "name": "[backend] debug npm run start", 25 | "command": "npm run start --workspace backend", 26 | "request": "launch", 27 | "type": "node-terminal" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "non-relative", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": "explicit", 5 | "source.removeUnusedImports": "explicit" 6 | }, 7 | "editor.formatOnSave": true 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.12.2 2 | 3 | # Setze das Arbeitsverzeichnis im Container 4 | WORKDIR /usr/src/app 5 | 6 | # Kompilat kopieren 7 | COPY backend/dist ./ 8 | 9 | # Installiere die Abhängigkeiten 10 | RUN npm install --no-package-lock --include=prod 11 | 12 | # Setze die Umgebungsvariable PORT 13 | ENV PORT=5000 14 | 15 | # Exponiere den Port, auf dem die Anwendung läuft 16 | EXPOSE 5000 17 | 18 | # Starte die Anwendung 19 | CMD ["node", "main.js"] -------------------------------------------------------------------------------- /GMCP.md: -------------------------------------------------------------------------------- 1 | # GMCP support of WEbmud3 (to be snychronized with project) 2 | 3 | * UNItopia support: https://github.com/unitopia-de/client-plugins/wiki/GMCP 4 | 5 | In UNItopia there is now more modules and messages supported, here now following the status: 6 | ### Module Core, MUD => Client 7 | - [x] Core.Ping: Fully Implemented with a button for GMCP-Ping. 8 | - [ ] Core.Goodbye (Parameter "goodbye-message"): Partially Implemented. E.g. useful for recycle ressources. 9 | ### Module Core, Client => MUD 10 | - [x] Core.Hello: Implemented, e.g. { client: 'Webmud3', version: 'v0.0.24' } 11 | - [ ] Core.Supports.Set/Add/Remove: A flexible configuration is needed in the Webmud3, so that only modules are active, which are also shown. 12 | - [x] Core.Ping implemented: request ping back from server. 13 | ### Module Char: MUD => Client 14 | - [ ] Char.Name { "name": "Leo", "fullname": "Leo, der Goetterbote", "gender": "maennlich" } 15 | - [ ] Char.StatusVars { "race": "Rasse", "guild": "Gilde", "rank": "Gildenrang" } 16 | - [ ] Char.Status { "race": "Mensch", "guild": "Bardengilde", "rank": "Bannsaenger" } 17 | - [ ] Char.Vitals { "hp": 100, "maxhp": 150, "sp": "120", "maxsp": 120, "string": "AP:100/150 ZP:120/120" } 18 | - [ ] Char.Stats { "str": 85, "int": 100, "con": 80, "dex": 98 } 19 | ### Module Char: Client => MUD 20 | - [ ] Char.Login to be implemented in UNItopia first. 21 | ### Modul Char.Items MUD => Client (Inventory) 22 | - [ ] Char.Items.List { "location": "inv", "items": [ { "name": "Ein Gummigoettchen", "category": "Nahrung" } ] } 23 | - [ ] Char.Items.Add { "location": "inv", "item": { "name": "Ein Gummigoettchen", "category": "Nahrung" } } 24 | - [ ] Char.Items.Remove { "location": "inv", "item": { "name": "Ein Gummigoettchen", "category": "Nahrung" } } 25 | ### Modul Char.Items: Client => MUD (Inventory) 26 | - [ ] Char.Items.Inv TODO: request on partial refresh... 27 | ### Modul Comm 28 | 29 | ### Modul Numpad MUD => Client 30 | - [ ] Numpad.SendLevel { "prefix":"", "keys": { 'Numpad7': "nordwesten", ... }} 31 | ### Modul Numpad Client => MUD 32 | - [ ] Numpad.Update( "prefix":"", "key":"Numpad7", "value": "nordwesten" } 33 | - [ ] Numpad.GetAll 34 | - [ ] Numpad.GetLevel { "prefix":"" } 35 | 36 | ### Module Status: 37 | - [ ] Core 90% 38 | - [ ] Char 39 | - [ ] Char.Items 40 | -------------------------------------------------------------------------------- /LINKS.txt: -------------------------------------------------------------------------------- 1 | node express angular2+: 2 | https://stackoverflow.com/questions/42895585/hooking-up-express-js-with-angular-cli-in-dev-environment 3 | 4 | ng2 socketio node mongodb: 5 | https://github.com/REPTILEHAUS/reptilehaus-ng2-socket.io-chat 6 | 7 | mudclient in node with telnet-stream: 8 | https://github.com/iliakan/mud-client 9 | 10 | telnet-stream itself: 11 | https://github.com/blinkdog/telnet-stream 12 | 13 | best practices node.js in docker: 14 | https://nodesource.com/blog/containerizing-node-js-applications-with-docker/ 15 | 16 | socket.on('error', (error) 17 | https://socket.io/docs/server-api/#event-error 18 | socket.on('disconnecting', (reason) 19 | socket.on('reconnect_attempt', (attemptNumber) => { 20 | socket.id 21 | 22 | http://ascii-table.com/ansi-escape-sequences.php 23 | 24 | https://en.wikipedia.org/wiki/ANSI_escape_code 25 | 26 | http://www.inwap.com/pdp10/ansicode.txt 27 | 28 | jansi: 29 | https://github.com/fusesource/jansi/blob/master/jansi/src/main/java/org/fusesource/jansi/Ansi.java 30 | 31 | ngClass, ngStyle,style.color 32 | https://stackoverflow.com/questions/44429104/angular2-set-a-different-color-to-an-element-depending-on-value 33 | 34 | ANSI in jquery: 35 | https://codereview.stackexchange.com/questions/2719/jquery-to-display-ansi-graphics 36 | 37 | https://ace.c9.io/ 38 | https://www.npmjs.com/package/ng2-ace-editor 39 | 40 | https://microsoft.github.io/language-server-protocol/overview 41 | 42 | create a server configuration in angular:(Done) 43 | https://angular-book.dev/ch10-03-loading-configuration-file.html 44 | https://levelup.gitconnected.com/angular-dynamic-routing-299c04ca75b1 45 | https://stackoverflow.com/questions/38112891/angular-2-set-base-href-dynamically 46 | 47 | 48 | /* eslint @typescript-eslint/no-this-alias: "warn" */ 49 | /* Object.prototype.hasOwnProperty.call(gmcp_support, element) */ 50 | /* eslint @typescript-eslint/ban-types: "warn" */ 51 | /* eslint @typescript-eslint/no-empty-function: "warn" */ 52 | 53 | (node:1) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [TLSSocket]. Use emitter.setMaxListeners() to increase limit 54 | 55 | missing packets: 56 | "ngx-device-detector": "^6.0.2", 57 | "primeng": "^16.7.0", -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": ["plugin:@typescript-eslint/recommended"], 5 | "parserOptions": { "ecmaVersion": 2020, "sourceType": "module" }, 6 | "plugins": ["@typescript-eslint", "simple-import-sort", "import"], 7 | "rules": { 8 | "simple-import-sort/imports": "warn", 9 | "simple-import-sort/exports": "warn", 10 | "import/extensions": [ 11 | "error", 12 | "ignorePackages", 13 | { 14 | "js": "always", 15 | "ts": "never" 16 | } 17 | ], 18 | "padding-line-between-statements": [ 19 | "warn", 20 | { "blankLine": "always", "prev": "expression", "next": "*" }, 21 | { "blankLine": "always", "prev": "const", "next": "*" }, 22 | { "blankLine": "always", "prev": "if", "next": "*" }, 23 | { "blankLine": "always", "prev": "block-like", "next": "*" } 24 | ] 25 | }, 26 | "overrides": [ 27 | { 28 | "files": ["**/*.test.js", "**/*.test.ts", "**/*.spec.js", "**/*.spec.ts"], 29 | "rules": { 30 | // This is very important since tests do not use the same import extensions as the rest of the codebase 31 | "import/extensions": "off" 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /backend/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | testMatch: [ 4 | "**/__tests__/**/*.+(ts|tsx|js)", 5 | "**/?(*.)+(spec|test).+(ts|tsx|js)", 6 | ], 7 | transform: { 8 | "^.+\\.(ts|tsx)$": [ 9 | "ts-jest", 10 | { 11 | diagnostics: { 12 | ignoreCodes: [1343], 13 | }, 14 | astTransformers: { 15 | before: [ 16 | { 17 | path: "ts-jest-mock-import-meta", // or, alternatively, 'ts-jest-mock-import-meta' directly, without node_modules. 18 | options: { 19 | metaObjectReplacement: () => ({ 20 | url: "https://www.dummy-url.com", 21 | }), 22 | }, 23 | }, 24 | ], 25 | }, 26 | }, 27 | ], 28 | }, 29 | extensionsToTreatAsEsm: [".ts"], 30 | testEnvironment: "node", 31 | // Notwendig, damit die Dateiendung .js nicht an den Dateinamen angehängt wird 32 | moduleNameMapper: { 33 | "^(.+).js$": "$1", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webmud3/backend", 3 | "version": "1.0.0-alpha", 4 | "description": "Webmud3 Backend Service", 5 | "engines": { 6 | "node": "20.12.2" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/unitopia-de/webmud3.git" 11 | }, 12 | "main": "main.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "npm run clean && tsc", 18 | "build:watch": "npm run clean && tsc --watch", 19 | "clean": "rimraf ./dist", 20 | "format": "prettier --write \"src/**/*.ts\"", 21 | "lint": "eslint ./src --ext .ts", 22 | "lint:fix": "eslint ./src --ext .ts --fix", 23 | "postbuild": "cpx package.json dist && cpx package-lock.json dist", 24 | "start": "npm run build && node dist/main.js", 25 | "serve": "node dist/main.js", 26 | "test": "jest --config=jest.config.cjs" 27 | }, 28 | "dependencies": { 29 | "ace-builds": "~1.33.1", 30 | "body-parser": "~1.20.2", 31 | "cookie-session": "~2.1.0", 32 | "cors": "~2.8.5", 33 | "dotenv": "^16.4.5", 34 | "express": "~4.19.2", 35 | "socket.io": "~4.7.5", 36 | "socket.io-client": "~4.7.5", 37 | "telnet-stream": "~1.1.0", 38 | "url": "~0.11.3", 39 | "uuid": "~9.0.1" 40 | }, 41 | "devDependencies": { 42 | "@types/cookie-session": "~2.0.49", 43 | "@types/express": "~4.17.21", 44 | "@types/jest": "~29.5.12", 45 | "@types/node": "20.12.2", 46 | "@types/source-map-support": "~0.5.10", 47 | "@types/uuid": "~9.0.8", 48 | "@typescript-eslint/eslint-plugin": "~7.8.0", 49 | "@typescript-eslint/parser": "~7.8.0", 50 | "cpx": "~1.5.0", 51 | "eslint": "~8.57.0", 52 | "eslint-config-standard": "~17.1.0", 53 | "eslint-plugin-import": "~2.29.1", 54 | "eslint-plugin-node": "~11.1.0", 55 | "eslint-plugin-simple-import-sort": "~12.1.0", 56 | "jest": "~29.7.0", 57 | "prettier": "~3.2.5", 58 | "rimraf": "~5.0.7", 59 | "source-map-support": "~0.5.21", 60 | "ts-jest": "~29.1.2", 61 | "ts-jest-mock-import-meta": "^1.2.0", 62 | "typescript": "~5.4.5", 63 | "winston": "~3.13.0" 64 | }, 65 | "type": "module", 66 | "author": "Myonara", 67 | "license": "MIT" 68 | } 69 | -------------------------------------------------------------------------------- /backend/src/cleanup.ts: -------------------------------------------------------------------------------- 1 | // Todo[myst]: Re-Enable this feature 2 | 3 | // // Object to capture process exits and call app specific cleanup function 4 | 5 | // function noOp() { 6 | // console.log('Empty Cleanup'); 7 | // } 8 | 9 | // exports.Cleanup = function Cleanup(callback) { 10 | // // attach user callback to the process event emitter 11 | // // if no callback, it will still exit gracefully on Ctrl-C 12 | // callback = callback || noOp; 13 | // process.on('cleanup', callback); 14 | 15 | // // do app specific cleaning before exiting 16 | // process.on('exit', function () { 17 | // process.emit('cleanup'); 18 | // }); 19 | 20 | // // catch ctrl+c event and exit normally 21 | // process.on('SIGINT', function () { 22 | // console.log('Ctrl-C...'); 23 | // process.emit('cleanup'); 24 | // process.exit(2); 25 | // }); 26 | 27 | // // catch SIGTERM event and exit normally (docker exit!) 28 | // process.on('SIGTERM', function () { 29 | // console.log('SIGTERM ...'); 30 | // process.emit('cleanup'); 31 | // process.exit(2); 32 | // }); 33 | 34 | // // catches "kill pid" (for example: nodemon restart) 35 | // process.on('SIGUSR1', function () { 36 | // console.log('SIGUSR1...'); 37 | // process.emit('cleanup'); 38 | // process.exit(2); 39 | // }); 40 | // process.on('SIGUSR2', function () { 41 | // console.log('SIGUSR2...'); 42 | // process.emit('cleanup'); 43 | // process.exit(2); 44 | // }); 45 | 46 | // // catch uncaught exceptions, trace, then exit normally 47 | // process.on('uncaughtException', function (e) { 48 | // console.log('Uncaught Exception...'); 49 | // console.log(e.stack); 50 | // process.exit(99); 51 | // }); 52 | // }; 53 | -------------------------------------------------------------------------------- /backend/src/core/environment/environment.ts: -------------------------------------------------------------------------------- 1 | import { config as configureEnvironment } from 'dotenv'; 2 | 3 | import { logger } from '../../shared/utils/logger.js'; 4 | import { IEnvironment } from './types/environment.js'; 5 | import { getEnvironmentVariable } from './utils/get-environment-variable.js'; 6 | import { resolveModulePath } from './utils/resolve-modulepath.js'; 7 | 8 | /** 9 | * Environment class to handle environment variables and application settings. 10 | * Reads the environment variables once oppon initialisation and provides them as properties. 11 | */ 12 | export class Environment implements IEnvironment { 13 | private static instance: Environment; 14 | 15 | public readonly host: string; 16 | public readonly port: number; 17 | public readonly telnetHost: string; 18 | public readonly telnetPort: number; 19 | public readonly telnetTLS: boolean; 20 | public readonly projectRoot: string; 21 | public readonly socketRoot: string; 22 | public readonly socketTimeout: number; 23 | public readonly environment: 'production' | 'development'; 24 | 25 | /** 26 | * Private constructor to enforce singleton pattern. 27 | * Initializes the environment variables. 28 | */ 29 | private constructor() { 30 | configureEnvironment(); 31 | 32 | this.host = String(getEnvironmentVariable('HOST', false, '0.0.0.0')); 33 | 34 | this.port = Number(getEnvironmentVariable('PORT', false, '5000')); 35 | 36 | this.telnetHost = String(getEnvironmentVariable('TELNET_HOST')); 37 | 38 | this.telnetPort = Number(getEnvironmentVariable('TELNET_PORT')); 39 | 40 | this.telnetTLS = 41 | getEnvironmentVariable( 42 | 'TELNET_TLS', 43 | false, 44 | 'false', 45 | )?.toLocaleLowerCase() === 'true'; 46 | 47 | this.socketRoot = String(getEnvironmentVariable('SOCKET_ROOT')); 48 | 49 | this.socketTimeout = Number( 50 | getEnvironmentVariable('SOCKET_TIMEOUT', false, '900000'), 51 | ); 52 | 53 | const environment = String( 54 | getEnvironmentVariable('ENVIRONMENT', false, 'production'), 55 | ).toLocaleLowerCase(); 56 | 57 | if (environment !== 'production' && environment !== 'development') { 58 | throw new Error( 59 | 'Environment variable "ENVIRONMENT" must be either "production" or "development" or unset.', 60 | ); 61 | } 62 | 63 | this.environment = environment; 64 | 65 | this.projectRoot = resolveModulePath('../../../main.js'); 66 | 67 | logger.info('[Environment] initialized', this); 68 | } 69 | 70 | /** 71 | * Gets the singleton instance of the Environment class. 72 | * @returns {Environment} The instance of the Environment class. 73 | */ 74 | public static getInstance(): Environment { 75 | return this.instance || (this.instance = new this()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/core/environment/types/environment-keys.ts: -------------------------------------------------------------------------------- 1 | export type EnvironmentKeys = 2 | | 'HOST' // Optional | defaults to '0.0.0.0' | the IP the backend will listen for 3 | | 'PORT' // Optional | defaults to 5000 | the PORT the backend will listen for 4 | | 'TELNET_HOST' // Required | the IP of your MUD 5 | | 'TELNET_PORT' // Required | the PORT of your MUD 6 | | 'TELNET_TLS' // Optional | defaults to 'false' | set this to true if you want a secure connection 7 | | 'SOCKET_TIMEOUT' // in milliseconds | default: 900000 (15 min) | determines how long messages are buffed for the disconnected frontend and when the telnet connection is closed 8 | | 'SOCKET_ROOT' // Required | the named socket for 9 | | 'ENVIRONMENT'; // Optional | Enables Debug REST Endpoint /api/info 10 | -------------------------------------------------------------------------------- /backend/src/core/environment/types/environment.ts: -------------------------------------------------------------------------------- 1 | export interface IEnvironment { 2 | readonly telnetHost: string; 3 | readonly telnetPort: number; 4 | readonly telnetTLS: boolean; 5 | 6 | readonly projectRoot: string; 7 | readonly socketRoot: string; 8 | 9 | readonly socketTimeout: number; 10 | 11 | readonly environment: 'production' | 'development'; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/core/environment/utils/get-environment-variable.spec.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentKeys } from '../types/environment-keys'; 2 | import { getEnvironmentVariable } from './get-environment-variable'; 3 | 4 | describe('getEnvironmentVariable', () => { 5 | const ENV_KEY = 'TEST_ENV_KEY' as EnvironmentKeys; 6 | 7 | beforeEach(() => { 8 | delete process.env[ENV_KEY]; 9 | }); 10 | 11 | it('should return the value of the environment variable if set', () => { 12 | process.env[ENV_KEY] = 'test_value'; 13 | 14 | const value = getEnvironmentVariable(ENV_KEY); 15 | 16 | expect(value).toBe('test_value'); 17 | }); 18 | 19 | it('should throw an error if the environment variable is not set and throwError is true', () => { 20 | expect(() => getEnvironmentVariable(ENV_KEY)).toThrow( 21 | `Environment variable ${ENV_KEY} is not set`, 22 | ); 23 | }); 24 | 25 | it('should return the default value if the environment variable is not set and throwError is false', () => { 26 | const defaultValue = 'default_value'; 27 | 28 | const value = getEnvironmentVariable(ENV_KEY, false, defaultValue); 29 | 30 | expect(value).toBe(defaultValue); 31 | }); 32 | 33 | it('should return null if the environment variable is not set, throwError is false, and no default value is provided', () => { 34 | const value = getEnvironmentVariable(ENV_KEY, false); 35 | 36 | expect(value).toBeNull(); 37 | }); 38 | 39 | // Todo[myst] renable this tests by mocking the new logger 40 | // it('should log a warning if the environment variable is not set, throwError is false, and no default value is provided', () => { 41 | // console.warn = jest.fn(); 42 | 43 | // getEnvironmentVariable(ENV_KEY, false); 44 | 45 | // expect(console.warn).toHaveBeenCalledWith( 46 | // `Environment variable ${ENV_KEY} is not set and no default value provided.`, 47 | // ); 48 | // }); 49 | 50 | // it('should log a warning if the environment variable is not set and a default value is provided', () => { 51 | // const defaultValue = 'default_value'; 52 | 53 | // console.warn = jest.fn(); 54 | 55 | // getEnvironmentVariable(ENV_KEY, false, defaultValue); 56 | 57 | // expect(console.warn).toHaveBeenCalledWith( 58 | // `Environment variable ${ENV_KEY} is not set. Using default value: ${defaultValue}`, 59 | // ); 60 | // }); 61 | }); 62 | -------------------------------------------------------------------------------- /backend/src/core/environment/utils/get-environment-variable.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../../shared/utils/logger.js'; 2 | import { EnvironmentKeys } from '../types/environment-keys.js'; 3 | 4 | /** 5 | * Retrieves an environment variable. 6 | * @param {EnvironmentKeys} env - The environment variable key. 7 | * @returns {string} The value of the environment variable. 8 | * @throws Will throw an error if the environment variable is not set. 9 | */ 10 | export function getEnvironmentVariable(env: EnvironmentKeys): string; 11 | 12 | /** 13 | * Retrieves an environment variable with an optional default value as backup. 14 | * @param {EnvironmentKeys} env - The environment variable key. 15 | * @param {boolean} throwError - Whether to throw an error if the variable is not set. This must be set to false in this case. 16 | * @param {string} [defaultValue] - The default value to use if the variable is not set. 17 | * @returns {string | null} The value of the environment variable, or the default value if not set. 18 | */ 19 | export function getEnvironmentVariable( 20 | env: EnvironmentKeys, 21 | throwError: false, 22 | defaultValue?: string, 23 | ): string | null; 24 | 25 | /** 26 | * Retrieves an environment variable with optional error throwing and default value. 27 | * @param {EnvironmentKeys} env - The environment variable key. 28 | * @param {boolean} [throwError=true] - Whether to throw an error if the variable is not set. 29 | * @param {string} [defaultValue] - The default value to use if the variable is not set and throwError is false. 30 | * @returns {string | null} The value of the environment variable, or the default value if not set and throwError is false. 31 | * @throws Will throw an error if the environment variable is not set and throwError is true. 32 | */ 33 | export function getEnvironmentVariable( 34 | env: EnvironmentKeys, 35 | throwError: boolean = true, 36 | defaultValue?: string, 37 | ): string | null { 38 | const value = process.env[env]; 39 | 40 | if (value === undefined || value === null || value === '') { 41 | if (throwError) { 42 | throw new Error(`Environment variable ${env} is not set`); 43 | } else { 44 | if (defaultValue !== undefined) { 45 | logger.warn( 46 | `[Environment] variable ${env} is not set. Using default value: ${defaultValue}`, 47 | ); 48 | 49 | return defaultValue; 50 | } else { 51 | logger.warn( 52 | `[Environment] variable ${env} is not set and no default value provided.`, 53 | ); 54 | 55 | return null; 56 | } 57 | } 58 | } 59 | 60 | return value; 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/core/environment/utils/resolve-modulepath.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | /** 5 | * This function is isolated since it uses the import.meta object. 6 | */ 7 | export function resolveModulePath(importMetaUrl: string): string { 8 | const __filename = fileURLToPath(import.meta.resolve(importMetaUrl)); 9 | 10 | return dirname(__filename); 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/core/middleware/use-body-parser.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import { Express } from 'express'; 3 | 4 | export const useBodyParser = (app: Express) => { 5 | // app.use(bodyParser.urlencoded({ extended: true })); 6 | app.use(bodyParser.json()); 7 | }; 8 | -------------------------------------------------------------------------------- /backend/src/core/middleware/use-cookie-session.ts: -------------------------------------------------------------------------------- 1 | import session from 'cookie-session'; 2 | import { Express } from 'express'; 3 | 4 | export const useCookieSession = (app: Express, secret: string) => { 5 | app.use( 6 | session({ 7 | secret, 8 | // Todo[myst]: Diese beiden Properties gibt es nicht mehr in der neuesten Version von cookie-session - aber ich habe keine Zeit, das jetzt zu fixen 9 | // Wir wollen im besten Fall eh auf JWT umsteigen 10 | // resave: false, 11 | // saveUninitialized: true, 12 | }), 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/core/middleware/use-rest-endpoints.ts: -------------------------------------------------------------------------------- 1 | import { Express, Request, Response } from 'express'; 2 | 3 | import { TelnetControlSequences } from '../../features/telnet/types/telnet-control-sequences.js'; 4 | import { TelnetOptions } from '../../features/telnet/types/telnet-options.js'; 5 | import { logger } from '../../shared/utils/logger.js'; 6 | import { SocketManager } from '../sockets/socket-manager.js'; 7 | 8 | export const useRestEndpoints = ( 9 | app: Express, 10 | socketManager: SocketManager, 11 | ) => { 12 | app.use('/api/info', (req: Request, res: Response) => { 13 | logger.info(`[Middleware] [Rest] requested /api/info`); 14 | 15 | const connections = Object.entries(socketManager.mudConnections).flatMap( 16 | ([connectionKey, con]) => { 17 | const negotiations = Object.entries(con.telnet?.negotiations || {}).map( 18 | ([negotiationKey, negotiations]) => { 19 | const key = Number(negotiationKey); 20 | 21 | return { 22 | code: TelnetOptions[key], 23 | ...{ 24 | server: negotiations?.server 25 | ? TelnetControlSequences[negotiations?.server] 26 | : {}, 27 | }, 28 | ...{ 29 | client: negotiations?.client 30 | ? TelnetControlSequences[negotiations?.client] 31 | : {}, 32 | }, 33 | ...negotiations?.subnegotiation, 34 | }; 35 | }, 36 | ); 37 | 38 | return { 39 | connection: connectionKey, 40 | telnet: { 41 | connected: 42 | con.telnet?.isConnected === undefined 43 | ? 'no instance' 44 | : con.telnet.isConnected, 45 | negotiations, 46 | }, 47 | }; 48 | }, 49 | ); 50 | 51 | res.send(connections); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /backend/src/core/middleware/use-sockets.ts: -------------------------------------------------------------------------------- 1 | import { Server as HttpServer } from 'http'; 2 | import { Server as HttpsServer } from 'https'; 3 | 4 | import { Environment } from '../environment/environment.js'; 5 | import { SocketManager } from '../sockets/socket-manager.js'; 6 | 7 | export const useSockets = ( 8 | httpServer: HttpServer | HttpsServer, 9 | environment: Environment, 10 | ) => { 11 | return new SocketManager(httpServer, { 12 | telnetHost: environment.telnetHost, 13 | telnetPort: environment.telnetPort, 14 | useTelnetTls: environment.telnetTLS, 15 | socketRoot: environment.socketRoot, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/core/middleware/use-static-files.ts: -------------------------------------------------------------------------------- 1 | import express, { Express } from 'express'; 2 | import path from 'path'; 3 | 4 | import { logger } from '../../shared/utils/logger.js'; 5 | import { Environment } from '../environment/environment.js'; 6 | 7 | export const useStaticFiles = (app: Express, folder: string) => { 8 | const assetPath = path.join(Environment.getInstance().projectRoot, folder); 9 | 10 | logger.info( 11 | `[Middleware] [Static-Files] Serving static files from ${assetPath}`, 12 | ); 13 | 14 | app.use( 15 | express.static(path.join(Environment.getInstance().projectRoot, folder)), 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/core/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import { Express, Request, Response } from 'express'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // import authRoutes from '../../features/auth/auth-routes.js'; 6 | import { logger } from '../../shared/utils/logger.js'; 7 | import { Environment } from '../environment/environment.js'; 8 | 9 | export const useRoutes = (app: Express) => { 10 | // app.use('/api/auth', authRoutes); 11 | 12 | app.get('/manifest.webmanifest', function (req: Request, res: Response) { 13 | logger.info(`[Routes] requested manifest.webmanifest`); 14 | 15 | fs.readFile( 16 | path.join(__dirname, 'dist', 'manifest.webmanifest'), 17 | function (err, data) { 18 | if (err) { 19 | res.sendStatus(404); 20 | } else { 21 | res.send(data); 22 | } 23 | }, 24 | ); 25 | }); 26 | 27 | app.get('/ace/*', (req: Request, res: Response) => { 28 | const ip = 29 | req.headers['x-forwarded-for'] || 30 | req.connection.remoteAddress || 31 | req.socket.remoteAddress || 32 | (req.socket ? req.socket.remoteAddress : null); 33 | 34 | const mypath = req.path.substr(5); 35 | 36 | logger.debug('ACE Path:', { real_ip: ip, path: mypath }); 37 | 38 | res.sendFile( 39 | path.join( 40 | __dirname, 41 | 'node_modules/ace-builds/src-min-noconflict/' + mypath, 42 | ), 43 | ); 44 | }); 45 | 46 | app.get('*', (req: Request, res: Response) => { 47 | logger.info(`[Routes] requested * - delivering index.html`); 48 | 49 | res.sendFile( 50 | path.join(Environment.getInstance().projectRoot, 'wwwroot/index.html'), 51 | ); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /backend/src/core/sockets/types/client-to-server-events.ts: -------------------------------------------------------------------------------- 1 | export interface ClientToServerEvents { 2 | mudConnect: () => void; 3 | mudDisconnect: () => void; 4 | mudInput: (data: string) => void; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/core/sockets/types/inter-server-events.ts: -------------------------------------------------------------------------------- 1 | export interface InterServerEvents { 2 | // ping: () => void; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/core/sockets/types/mud-connections.ts: -------------------------------------------------------------------------------- 1 | import { TelnetClient } from '../../../features/telnet/telnet-client.js'; 2 | 3 | export type MudConnections = { 4 | [socketId: string]: { 5 | telnet: TelnetClient | undefined; 6 | connectionTimer: NodeJS.Timeout | undefined; 7 | echo: boolean; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/core/sockets/types/server-to-client-events.ts: -------------------------------------------------------------------------------- 1 | export interface ServerToClientEvents { 2 | mudOutput: (data: string) => void; 3 | mudDisconnected: () => void; 4 | mudConnected: () => void; 5 | setEchoMode: (showEchos: boolean) => void; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/features/auth/auth-routes.ts: -------------------------------------------------------------------------------- 1 | // import { Router } from 'express'; 2 | // import { AuthController } from './auth.js'; 3 | 4 | // const router = Router(); 5 | 6 | // // middleware that is specific to this router 7 | // router.use(function (req, res, next) { 8 | // const ip = 9 | // req.headers['x-forwarded-for'] || 10 | // req.connection.remoteAddress || 11 | // req.socket.remoteAddress || 12 | // (req.socket ? req.socket.remoteAddress : null); 13 | 14 | // console.log(ip, '/api/auth', req.method, req.url); 15 | 16 | // next(); 17 | // }); 18 | 19 | // router.route('/login').post(AuthController.logon).get(AuthController.loggedon); 20 | 21 | // router.route('/logout').post(AuthController.logout); 22 | 23 | // export default router; 24 | -------------------------------------------------------------------------------- /backend/src/features/auth/auth.ts: -------------------------------------------------------------------------------- 1 | // import { Request, Response } from 'express'; 2 | 3 | // import { RPCClient } from '../websockets/rpc-client.js'; 4 | 5 | // export const AuthController = { 6 | // logon: function (req: Request, res: Response) { 7 | // if (req.body) { 8 | // // TODO captcha... 9 | // console.log('AuthController->logon ', req.body.logonname); 10 | 11 | // RPCClient.getInstance().logon( 12 | // req.body.logonname, 13 | // req.body.password, 14 | // function (err, result) { 15 | // if (err) { 16 | // res.status(401).send(err); // Bad request. 17 | // } else if (typeof result === 'string') { 18 | // res.status(403).send(result); // Wrong pw/auth error 19 | // } else { 20 | // req.session = { 21 | // user: { 22 | // logonname: result?.name, 23 | // adminp: result?.adminp, 24 | // }, 25 | // }; 26 | 27 | // res 28 | // .status(201) 29 | // .send({ logonname: result?.name, adminp: result?.adminp }); 30 | // } 31 | // }, 32 | // ); 33 | // } else { 34 | // // TODO logging icl. real_ip!! 35 | // res.status(400).send({ 36 | // // Bad request. 37 | // msgid: 'AUC02', 38 | // msg: 'You need a html body.', 39 | // msgclass: 'htmlError', 40 | // }); 41 | // } 42 | // }, 43 | 44 | // loggedon: function (req: Request, res: Response) { 45 | // if (req.session?.user) { 46 | // res.status(201).send({ loggedIn: true, adminp: req.session.user.adminp }); 47 | // } else { 48 | // res.status(201).send({ loggedIn: false, adminp: false }); 49 | // } 50 | // }, 51 | 52 | // logout: function (req: Request, res: Response) { 53 | // req.session?.destroy((err: Error) => { 54 | // if (err) { 55 | // res.status(500).send('Could not log out.'); 56 | // } else { 57 | // res.status(201).send({ loggedOut: true }); 58 | // } 59 | // }); 60 | // }, 61 | // }; 62 | -------------------------------------------------------------------------------- /backend/src/features/telnet/types/telnet-control-sequences.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Telnet Control Sequences used for option negotiation. 3 | */ 4 | export enum TelnetControlSequences { 5 | /** 6 | * Confirm that the sender will start performing an option. 7 | */ 8 | WILL = 251, 9 | 10 | /** 11 | * Confirm that the sender will stop performing an option. 12 | */ 13 | WONT = 252, 14 | 15 | /** 16 | * Request or confirm the other party to start performing an option. 17 | */ 18 | DO = 253, 19 | 20 | /** 21 | * Demand the other party to stop performing an option. 22 | */ 23 | DONT = 254, 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/features/telnet/types/telnet-negotiation-result.ts: -------------------------------------------------------------------------------- 1 | import { TelnetControlSequences } from './telnet-control-sequences.js'; 2 | import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js'; 3 | 4 | export type TelnetNegotiationResult = { 5 | subNegotiationResult?: TelnetSubnegotiationResult; 6 | 7 | controlSequence: TelnetControlSequences; 8 | }; 9 | -------------------------------------------------------------------------------- /backend/src/features/telnet/types/telnet-negotiations.ts: -------------------------------------------------------------------------------- 1 | import { TelnetControlSequences } from './telnet-control-sequences.js'; 2 | import { TelnetOptions } from './telnet-options.js'; 3 | 4 | /** 5 | * Represents the state of negotiations for Telnet options, including both server 6 | * and client control sequences, as well as any subnegotiation data. 7 | */ 8 | export type TelnetNegotiations = { 9 | -readonly [key in keyof typeof TelnetOptions]?: { 10 | /** 11 | * The control sequence received from the server (DO, DON'T, WILL, WON'T). 12 | */ 13 | server?: TelnetControlSequences; 14 | 15 | /** 16 | * The control sequence sent by the client (DO, DON'T, WILL, WON'T). 17 | */ 18 | client?: TelnetControlSequences; 19 | 20 | /** 21 | * Optional subnegotiation data exchanged between the server and client. 22 | */ 23 | subnegotiation?: { 24 | /** 25 | * The data chunk sent by the server during subnegotiation. 26 | */ 27 | serverChunk?: string; 28 | 29 | /** 30 | * The data chunk sent by the client during subnegotiation. 31 | */ 32 | clientChunk?: string; 33 | 34 | /** 35 | * The client option used during subnegotiation (e.g., a charset or mode). 36 | */ 37 | clientOption?: string; 38 | }; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /backend/src/features/telnet/types/telnet-option-handler.ts: -------------------------------------------------------------------------------- 1 | import { TelnetNegotiationResult } from './telnet-negotiation-result.js'; 2 | import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js'; 3 | 4 | /** 5 | * A set of handler functions for managing Telnet option negotiation. 6 | * Each handler responds to a specific Telnet negotiation command (DO, DON'T, WILL, WON'T), 7 | * and optionally handles subnegotiation. 8 | */ 9 | export type TelnetOptionHandler = { 10 | /** 11 | * Intiate a negotiation with the server. 12 | * @returns {TelnetNegotiationResult} The control sequence (DO, DONT, WILL, WONT) sent back to the server. 13 | */ 14 | negotiate?: () => TelnetNegotiationResult; 15 | 16 | /** 17 | * Handles the "DO" command sent by the server, indicating that the server 18 | * wants the client to enable a particular option. 19 | * 20 | * @returns {TelnetNegotiationResult} The control sequence (WILL, WONT) sent back to the server. 21 | */ 22 | handleDo: () => TelnetNegotiationResult; 23 | 24 | /** 25 | * Handles the "DON'T" command sent by the server, indicating that the server 26 | * wants the client to disable a particular option. 27 | * 28 | * @returns {TelnetControlSequences} The control sequence (WILL, WONT) sent back to the server. 29 | */ 30 | handleDont: () => TelnetNegotiationResult; 31 | 32 | /** 33 | * Handles the "WILL" command sent by the server, indicating that the server 34 | * is willing to enable a particular option. 35 | * 36 | * @returns {TelnetNegotiationResult} The control sequence (DO, DONT) sent back to the server. 37 | */ 38 | handleWill: () => TelnetNegotiationResult; 39 | 40 | /** 41 | * Handles the "WON'T" command sent by the server, indicating that the server 42 | * is unwilling to enable a particular option. 43 | * 44 | * @returns {TelnetNegotiationResult} The control sequence (DO, DONT) sent back to the server. 45 | */ 46 | handleWont: () => TelnetNegotiationResult; 47 | 48 | /** 49 | * Handles the subnegotiation message sent by the server. 50 | * 51 | * @param {Buffer} serverChunk - The data chunk sent by the server during subnegotiation. 52 | * @returns {TelnetSubnegotiationResult | null} The subnegotiation result, or null if not applicable. 53 | */ 54 | handleSub?: (serverChunk: Buffer) => TelnetSubnegotiationResult; 55 | }; 56 | -------------------------------------------------------------------------------- /backend/src/features/telnet/types/telnet-subnegotiation-result.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the result of a Telnet subnegotiation. 3 | * It contains the data that the client sends back to the server during subnegotiation. 4 | * The client option may return null if the subnegotiation is not applicable. 5 | */ 6 | export type TelnetSubnegotiationResult = { 7 | /** 8 | * The chunk of data that the client sends during subnegotiation. 9 | */ 10 | clientChunk: Buffer; 11 | /** 12 | * The client option used during subnegotiation (e.g., a charset or mode). 13 | */ 14 | clientOption: string; 15 | } | null; 16 | -------------------------------------------------------------------------------- /backend/src/features/telnet/utils/handle-echo-option.ts: -------------------------------------------------------------------------------- 1 | import { TelnetSocket } from 'telnet-stream'; 2 | 3 | import { TelnetControlSequences } from '../types/telnet-control-sequences.js'; 4 | import { TelnetNegotiationResult } from '../types/telnet-negotiation-result.js'; 5 | import { TelnetOptionHandler } from '../types/telnet-option-handler.js'; 6 | import { TelnetOptions } from '../types/telnet-options.js'; 7 | 8 | const handleEchoDo = (socket: TelnetSocket) => (): TelnetNegotiationResult => { 9 | socket.writeWill(TelnetOptions.TELOPT_ECHO); 10 | 11 | return { controlSequence: TelnetControlSequences.WILL }; 12 | }; 13 | 14 | const handleEchoDont = 15 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 16 | socket.writeWont(TelnetOptions.TELOPT_ECHO); 17 | 18 | return { controlSequence: TelnetControlSequences.WONT }; 19 | }; 20 | 21 | const handleEchoWill = 22 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 23 | socket.writeDo(TelnetOptions.TELOPT_ECHO); 24 | 25 | return { controlSequence: TelnetControlSequences.DO }; 26 | }; 27 | 28 | const handleEchoWont = 29 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 30 | socket.writeDont(TelnetOptions.TELOPT_ECHO); 31 | 32 | return { controlSequence: TelnetControlSequences.DONT }; 33 | }; 34 | 35 | export const handleEchoOption = (socket: TelnetSocket): TelnetOptionHandler => { 36 | return { 37 | handleDo: handleEchoDo(socket), 38 | handleDont: handleEchoDont(socket), 39 | handleWill: handleEchoWill(socket), 40 | handleWont: handleEchoWont(socket), 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /backend/src/features/telnet/utils/handle-naws-option.ts: -------------------------------------------------------------------------------- 1 | import { TelnetSocket } from 'telnet-stream'; 2 | 3 | import { sizeToBuffer } from '../../../shared/utils/size-to-buffer.js'; 4 | import { TelnetControlSequences } from '../types/telnet-control-sequences.js'; 5 | import { TelnetNegotiationResult } from '../types/telnet-negotiation-result.js'; 6 | import { TelnetOptionHandler } from '../types/telnet-option-handler.js'; 7 | import { TelnetOptions } from '../types/telnet-options.js'; 8 | 9 | const DEFAULT_VIEWPORT_WIDTH = 80; 10 | 11 | const DEFAULT_VIEWPORT_HEIGHT = 25; 12 | 13 | const handleNawsDo = (socket: TelnetSocket) => (): TelnetNegotiationResult => { 14 | socket.writeWill(TelnetOptions.TELOPT_NAWS); 15 | 16 | const buffer = sizeToBuffer(DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT); 17 | 18 | socket.writeSub(TelnetOptions.TELOPT_NAWS, buffer); 19 | 20 | return { 21 | controlSequence: TelnetControlSequences.WONT, 22 | subNegotiationResult: { 23 | clientChunk: buffer, 24 | clientOption: `${DEFAULT_VIEWPORT_WIDTH}x${DEFAULT_VIEWPORT_HEIGHT}`, 25 | }, 26 | }; 27 | }; 28 | 29 | const handleNawsDont = 30 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 31 | socket.writeWont(TelnetOptions.TELOPT_NAWS); 32 | 33 | return { controlSequence: TelnetControlSequences.WONT }; 34 | }; 35 | 36 | const handleNawsWill = 37 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 38 | socket.writeDont(TelnetOptions.TELOPT_NAWS); 39 | 40 | // We do not allow the MUD to set the window size since this makes no sense in our responsive client 41 | // so any subnogitiation is ignored. 42 | return { controlSequence: TelnetControlSequences.DONT }; 43 | }; 44 | 45 | const handleNawsWont = 46 | (socket: TelnetSocket) => (): TelnetNegotiationResult => { 47 | socket.writeDont(TelnetOptions.TELOPT_NAWS); 48 | 49 | return { controlSequence: TelnetControlSequences.DONT }; 50 | }; 51 | 52 | export const handleNawsOption = (socket: TelnetSocket): TelnetOptionHandler => { 53 | return { 54 | handleDo: handleNawsDo(socket), 55 | handleDont: handleNawsDont(socket), 56 | handleWill: handleNawsWill(socket), 57 | handleWont: handleNawsWont(socket), 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /backend/src/features/telnet/utils/log-negotiation.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../../shared/utils/logger.js'; 2 | import { TelnetOptions } from '../types/telnet-options.js'; 3 | 4 | export function logNegotiation( 5 | perspective: 'Received' | 'Send', 6 | action: string, 7 | option: number, 8 | data?: Buffer, 9 | ) { 10 | // be careful since typescript does not recognize the value as undefined if you provide a number not in the enum 11 | const opt = TelnetOptions[option] as string | undefined; 12 | 13 | logger.verbose( 14 | `[Telnet-Socket] ${perspective} ${action} for option ${opt ?? 'unknown (number: ' + option + ')'}`, 15 | data ? { data: data.toString() } : {}, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/features/telnet/utils/telnet-socket-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { TelnetSocket, TelnetSocketOptions } from 'telnet-stream'; 3 | 4 | import { logNegotiation } from './log-negotiation.js'; 5 | 6 | export class TelnetSocketWrapper extends TelnetSocket { 7 | public override writeDo(option: number): void { 8 | logNegotiation('Send', 'do', option); 9 | 10 | super.writeDo(option); 11 | } 12 | 13 | public override writeDont(option: number): void { 14 | logNegotiation('Send', 'dont', option); 15 | 16 | super.writeDont(option); 17 | } 18 | 19 | public override writeWill(option: number): void { 20 | logNegotiation('Send', 'will', option); 21 | 22 | super.writeWill(option); 23 | } 24 | 25 | public override writeWont(option: number): void { 26 | logNegotiation('Send', 'wont', option); 27 | 28 | super.writeWont(option); 29 | } 30 | 31 | public override writeSub(option: number, buffer: Buffer): void { 32 | logNegotiation('Send', 'sub', option, buffer); 33 | 34 | super.writeSub(option, buffer); 35 | } 36 | 37 | constructor(socket: Socket, options?: TelnetSocketOptions) { 38 | super(socket, options); 39 | 40 | this.on('will', (option) => logNegotiation('Received', 'will', option)); 41 | 42 | this.on('wont', (option) => logNegotiation('Received', 'wont', option)); 43 | 44 | this.on('do', (option) => logNegotiation('Received', 'do', option)); 45 | 46 | this.on('dont', (option) => logNegotiation('Received', 'dont', option)); 47 | 48 | this.on('sub', (option, chunkData) => 49 | logNegotiation('Received', 'sub', option, chunkData), 50 | ); 51 | 52 | this.on('command', (command) => 53 | logNegotiation('Received', 'command', command), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import sourceMaps from 'source-map-support'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | import { Environment } from './core/environment/environment.js'; 6 | import { useBodyParser } from './core/middleware/use-body-parser.js'; 7 | import { useRestEndpoints } from './core/middleware/use-rest-endpoints.js'; 8 | import { useSockets } from './core/middleware/use-sockets.js'; 9 | import { useStaticFiles } from './core/middleware/use-static-files.js'; 10 | import { useRoutes } from './core/routes/routes.js'; 11 | import { createHttpServer } from './shared/utils/create-http-server.js'; 12 | import { logger } from './shared/utils/logger.js'; 13 | 14 | sourceMaps.install(); 15 | 16 | const environment = Environment.getInstance(); 17 | 18 | const app = express(); 19 | 20 | const UNIQUE_SERVER_ID = uuidv4(); 21 | 22 | const httpServer = createHttpServer(app, {}); 23 | 24 | useBodyParser(app); 25 | 26 | // Todo[myst]: What does this bring to the table? 27 | // useCookieSession(app, secretConfig.mySessionKey); 28 | 29 | useStaticFiles(app, 'wwwroot'); 30 | 31 | const socketManager = useSockets(httpServer, environment); 32 | 33 | // Enable Debug Rest Endpoints in Development Mode 34 | if (environment.environment === 'development') { 35 | useRestEndpoints(app, socketManager); 36 | } 37 | 38 | useRoutes(app); 39 | 40 | // function myCleanup() { 41 | // console.log('Cleanup starts.'); 42 | // if (typeof MudConnections !== 'undefined') { 43 | // for (const key in MudConnections) { 44 | // // skip loop if the property is from prototype 45 | // if (!MudConnections.hasOwnProperty(key)) continue; 46 | // // get object. 47 | // const obj = MudConnections[key]; 48 | // // message to all frontends... 49 | // io.emit('mud-disconnected', key); 50 | // // disconnect gracefully. 51 | // obj.socket.end(); 52 | // } 53 | // } 54 | // console.log('Cleanup ends.'); 55 | // } 56 | 57 | httpServer.listen(environment.port, environment.host, 10000, () => { 58 | logger.info(`[Main] Server started on port ${environment.port}`, { 59 | UNIQUE_SERVER_ID, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /backend/src/shared/utils/create-http-server.ts: -------------------------------------------------------------------------------- 1 | import { Express } from 'express'; 2 | import fs from 'fs'; 3 | import { Server as HttpServer } from 'http'; 4 | import { Server as HttpsServer } from 'https'; 5 | 6 | import { logger } from './logger.js'; 7 | 8 | // Todo[myst]: Ich will das eigentlich nicht hier haben, da man nicht beliebig in der Anwendung einfach mal http Server spawnen können sollte 9 | export function createHttpServer( 10 | app: Express, 11 | settings: { tls?: { cert: string; key: string } }, 12 | ): HttpServer | HttpsServer { 13 | if (settings.tls !== undefined) { 14 | const options = { 15 | key: fs.readFileSync(settings.tls.key), 16 | cert: fs.readFileSync(settings.tls.cert), 17 | }; 18 | 19 | logger.debug('SRV://5000 : INIT: https active'); 20 | 21 | return new HttpsServer(options, app); 22 | } else { 23 | return new HttpServer(app); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/shared/utils/is-buffer-encoding.spec.ts: -------------------------------------------------------------------------------- 1 | import { isBufferEncoding } from './is-buffer-encoding'; 2 | 3 | describe('isBufferEncoding', () => { 4 | // Test für gültige BufferEncodings 5 | test('should return true for valid BufferEncodings', () => { 6 | expect(isBufferEncoding('ascii')).toBe(true); 7 | 8 | expect(isBufferEncoding('utf8')).toBe(true); 9 | 10 | expect(isBufferEncoding('utf-8')).toBe(true); 11 | 12 | expect(isBufferEncoding('utf16le')).toBe(true); 13 | 14 | expect(isBufferEncoding('utf-16le')).toBe(true); 15 | 16 | expect(isBufferEncoding('ucs2')).toBe(true); 17 | 18 | expect(isBufferEncoding('ucs-2')).toBe(true); 19 | 20 | expect(isBufferEncoding('base64')).toBe(true); 21 | 22 | expect(isBufferEncoding('base64url')).toBe(true); 23 | 24 | expect(isBufferEncoding('latin1')).toBe(true); 25 | 26 | expect(isBufferEncoding('binary')).toBe(true); 27 | 28 | expect(isBufferEncoding('hex')).toBe(true); 29 | }); 30 | 31 | // Test für ungültige BufferEncodings 32 | test('should return false for invalid BufferEncodings', () => { 33 | expect(isBufferEncoding('UTF8')).toBe(false); // Großbuchstaben 34 | 35 | expect(isBufferEncoding('utf 8')).toBe(false); // Leerzeichen 36 | 37 | expect(isBufferEncoding('UTF-16')).toBe(false); // ungültige Kodierung 38 | 39 | expect(isBufferEncoding('utf-32')).toBe(false); // nicht unterstütztes Encoding 40 | 41 | expect(isBufferEncoding('')).toBe(false); // leerer String 42 | 43 | expect(isBufferEncoding('randomString')).toBe(false); // zufälliger String 44 | }); 45 | 46 | // Edge Case Test: Unterschied zwischen ähnlichen Kodierungen 47 | test('should handle similar but invalid encodings', () => { 48 | expect(isBufferEncoding('utf16')).toBe(false); // Kein 'le' Suffix 49 | 50 | expect(isBufferEncoding('ucs')).toBe(false); // kein '-2' 51 | 52 | expect(isBufferEncoding('utf_8')).toBe(false); // falsches Zeichen (Unterstrich statt Bindestrich) 53 | }); 54 | 55 | // Test für sehr lange Strings (Edge Case) 56 | test('should return false for excessively long strings', () => { 57 | const longString = 'a'.repeat(1000); // Sehr langer String 58 | 59 | expect(isBufferEncoding(longString)).toBe(false); 60 | }); 61 | 62 | // Test für Strings mit Sonderzeichen (Edge Case) 63 | test('should return false for strings with special characters', () => { 64 | expect(isBufferEncoding('utf8!')).toBe(false); // Ungültiges Sonderzeichen 65 | 66 | expect(isBufferEncoding('base64$')).toBe(false); // Ungültiges Sonderzeichen 67 | 68 | expect(isBufferEncoding('latin@1')).toBe(false); // Ungültiges Sonderzeichen 69 | }); 70 | 71 | // Test für undefined oder null (Edge Case) 72 | test('should return false for undefined or null', () => { 73 | expect(isBufferEncoding(undefined as unknown as string)).toBe(false); 74 | 75 | expect(isBufferEncoding(null as unknown as string)).toBe(false); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /backend/src/shared/utils/is-buffer-encoding.ts: -------------------------------------------------------------------------------- 1 | export function isBufferEncoding(encoding: string): encoding is BufferEncoding { 2 | return [ 3 | 'ascii', 4 | 'utf8', 5 | 'utf-8', 6 | 'utf16le', 7 | 'utf-16le', 8 | 'ucs2', 9 | 'ucs-2', 10 | 'base64', 11 | 'base64url', 12 | 'latin1', 13 | 'binary', 14 | 'hex', 15 | ].includes(encoding); 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/shared/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | // Comment this in if you want to log metadata 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const metadataFormat = winston.format((info) => { 6 | if (info.metadata && Object.keys(info.metadata).length > 0) { 7 | info.message += `\n${JSON.stringify(info.metadata)}`; 8 | } 9 | 10 | return info; 11 | }); 12 | 13 | /** 14 | * Hinweis zu den unterstützen Log-Leveln in der Priotiätsreihenfolge: 15 | * - error 16 | * - warn 17 | * - info 18 | * - http 19 | * - verbose 20 | * - debug 21 | * - silly 22 | **/ 23 | const logger = winston.createLogger({ 24 | level: 'debug', 25 | levels: winston.config.npm.levels, 26 | format: winston.format.combine( 27 | winston.format.timestamp({ 28 | format: 'YYYY-MM-DD HH:mm:ss', 29 | }), 30 | winston.format.metadata({ 31 | fillExcept: ['message', 'level', 'timestamp', 'label'], 32 | }), 33 | metadataFormat(), 34 | winston.format.printf( 35 | (info) => `[${info.timestamp}] [${info.level}] ${info.message}`, 36 | ), 37 | ), 38 | transports: [ 39 | new winston.transports.Console({ 40 | handleExceptions: true, 41 | handleRejections: true, 42 | format: winston.format.combine( 43 | winston.format.colorize(), 44 | winston.format.printf( 45 | (info) => `[${info.timestamp}] [${info.level}] ${info.message}`, 46 | ), 47 | ), 48 | }), 49 | ], 50 | exitOnError: false, 51 | }); 52 | 53 | export { logger }; 54 | -------------------------------------------------------------------------------- /backend/src/shared/utils/size-to-buffer.spec.ts: -------------------------------------------------------------------------------- 1 | import { sizeToBuffer } from './size-to-buffer'; 2 | 3 | describe('sizeToBuffer', () => { 4 | it('should convert width and height into a buffer of four bytes', () => { 5 | const buf = sizeToBuffer(0x1234, 0xabcd); 6 | 7 | expect([...buf]).toEqual([0x12, 0x34, 0xab, 0xcd]); 8 | }); 9 | 10 | it('should handle zeros correctly', () => { 11 | const buf = sizeToBuffer(0, 0); 12 | 13 | expect([...buf]).toEqual([0, 0, 0, 0]); 14 | }); 15 | 16 | it('should handle single byte values correctly', () => { 17 | const buf = sizeToBuffer(0x00ff, 0x00ff); 18 | 19 | expect([...buf]).toEqual([0, 255, 0, 255]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /backend/src/shared/utils/size-to-buffer.ts: -------------------------------------------------------------------------------- 1 | import { val16ToBuffer } from './val16-to-buffer.js'; 2 | 3 | /** 4 | * Converts two numeric width and height values into a Buffer encoding their values in 16-bit. 5 | * @param {number} w - The width value to encode. 6 | * @param {number} h - The height value to encode. 7 | * @returns {Buffer} A Buffer object containing the 16-bit encoded width and height. 8 | */ 9 | export function sizeToBuffer(w: number, h: number): Buffer { 10 | // Combine the 16-bit buffers for width and height into a single array. 11 | const result = [...val16ToBuffer(w), ...val16ToBuffer(h)]; 12 | 13 | // Convert the array of numbers into a Buffer and return it. 14 | return Buffer.from(result); 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/shared/utils/supported-encodings.ts: -------------------------------------------------------------------------------- 1 | export function mapToServerEncodings(charset: string): BufferEncoding | null { 2 | switch (charset) { 3 | case 'UTF-8': 4 | case 'UTF8': 5 | case 'utf8': 6 | case 'utf-8': 7 | return 'utf-8'; 8 | default: 9 | return null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/shared/utils/txt-to-buffer.ts: -------------------------------------------------------------------------------- 1 | // Todo[myst] unused function - was defined in telnet-client but not used, discuss 2 | export function txtToBuffer(text: string): Buffer { 3 | const result = []; 4 | 5 | let i = 0; 6 | text = encodeURI(text); 7 | 8 | while (i < text.length) { 9 | const c = text.charCodeAt(i++); 10 | 11 | if (c === 37) { 12 | // if it is a % sign, encode the following 2 bytes as a hex value 13 | result.push(parseInt(text.substr(i, 2), 16)); 14 | 15 | i += 2; 16 | } else { 17 | // otherwise, just the actual byte 18 | result.push(c); 19 | } 20 | } 21 | 22 | return Buffer.from(result); 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/shared/utils/val16-to-buffer.spec.ts: -------------------------------------------------------------------------------- 1 | import { val16ToBuffer } from './val16-to-buffer'; 2 | 3 | describe('val16ToBuffer', () => { 4 | it('should convert a number to two bytes and add to the array', () => { 5 | const output = val16ToBuffer(0x1234); 6 | 7 | expect(output).toEqual([0x12, 0x34]); 8 | }); 9 | 10 | it('should handle zero correctly', () => { 11 | const output = val16ToBuffer(0); 12 | 13 | expect(output).toEqual([0, 0]); 14 | }); 15 | 16 | it('should handle a single byte value correctly', () => { 17 | const output = val16ToBuffer(0x00ff); 18 | 19 | expect(output).toEqual([0, 255]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /backend/src/shared/utils/val16-to-buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a number into a 16-bit representation, split into two 8-bit values. 3 | * @param {number} val - The value to be converted into 16-bit. 4 | * @returns {number[]} An array with the high and low bytes of the 16-bit value. 5 | */ 6 | export function val16ToBuffer(val: number): number[] { 7 | // Return an array containing the high byte and the low byte of the value. 8 | return [(val & 0xff00) >> 8, val & 0xff]; 9 | } 10 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | ] 19 | } -------------------------------------------------------------------------------- /dockerfiles/ng_unitopia_test.dockerfile: -------------------------------------------------------------------------------- 1 | # based on node 10, alpine for least resource requirements. 2 | FROM node:20-alpine3.18 AS ng-build-stage 3 | 4 | # working dir in build stage 5 | WORKDIR /app 6 | 7 | # fetching packages and... 8 | COPY UI17/package*.json /app/ 9 | 10 | RUN echo https://alpine.mirror.wearetriple.com/v3.18/main > /etc/apk/repositories; \ 11 | echo https://alpine.mirror.wearetriple.com/v3.18/community >> /etc/apk/repositories 12 | 13 | # ... install them together with angular-cli, prequisite git included. 14 | RUN apk update && apk upgrade && \ 15 | apk add --no-cache bash git openssh \ 16 | && npm install --location=global @angular/cli \ 17 | && npm install 18 | 19 | # fetch the angular sources and stuff 20 | COPY ./UI17/ /app/ 21 | 22 | # create the output of the angular app 23 | RUN ng build --configuration development-unitopia --output-path=dist/out 24 | 25 | # produces the final node.js immage. 26 | FROM node:20-alpine3.18 AS webmud3 27 | 28 | # again a working dir... 29 | WORKDIR /app 30 | 31 | # fetch the backend source files... 32 | COPY ./backend/ /app/ 33 | 34 | #fetch the angular distribution for serving from node.js 35 | COPY --from=ng-build-stage /app/dist/out/ /app/dist/ 36 | 37 | # mkdir runs 38 | RUN mkdir /run/secrets \ 39 | && mkdir /run/db \ 40 | && npm install --only=prod 41 | 42 | CMD node server.js 43 | 44 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | # replace username/repo:tag with your name and image details 5 | image: myonara/webmud3:latest 6 | environment: 7 | NODE_ENV: 'production' 8 | SECRET_CONFIG: '/run/webmud3.json' 9 | MUD_CONFIG: '/run/mud_config_unitopia.json' 10 | WEBMUD3_DISTRIBUTION_TYPE: 'unitopia-prod' 11 | volumes: 12 | - "/UNItopia/ftpwww/webmud3/run:/run" 13 | # command: --tls-cert=/run/secrets/cert.pem --tls-key=/run/secrets/privkey.pem 14 | deploy: 15 | replicas: 1 16 | resources: 17 | limits: 18 | cpus: "0.1" 19 | memory: 50M 20 | labels: 21 | com.docker.lb.hosts: www.unitopia.de 22 | com.docker.lb.network: webnet 23 | com.docker.lb.port: 2018 24 | restart_policy: 25 | condition: on-failure 26 | ports: 27 | - "2018:5000" 28 | networks: 29 | - webnet 30 | networks: 31 | webnet: 32 | #driver: overlay 33 | attachable: true 34 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_local.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | # replace username/repo:tag with your name and image details 5 | image: myonara/webmud3:latest 6 | environment: 7 | NODE_ENV: 'production' 8 | # command: --tls-cert=/run/secrets/cert.pem --tls-key=/run/secrets/privkey.pem 9 | deploy: 10 | replicas: 1 11 | resources: 12 | limits: 13 | cpus: "0.1" 14 | memory: 50M 15 | labels: 16 | com.docker.lb.hosts: www.unitopia.de 17 | com.docker.lb.network: webnet 18 | com.docker.lb.port: 2018 19 | restart_policy: 20 | condition: on-failure 21 | delay: 20s 22 | max_attempts: 2 23 | window: 3600s 24 | ports: 25 | - "2018:5000" 26 | networks: 27 | - webnet 28 | networks: 29 | webnet: 30 | driver: overlay 31 | attachable: true 32 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_sb.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | image: myonara/webmud3:latest 5 | environment: 6 | NODE_ENV: 'production' 7 | SECRET_CONFIG: '/run/webmud3sb.json' 8 | MUD_CONFIG: '/run/mud_config_seifenblase.json' 9 | WEBMUD3_DISTRIBUTION_TYPE: 'seifenblase' 10 | volumes: 11 | - "/UNItopia/ftpwww/webmud3/run:/run" 12 | deploy: 13 | replicas: 1 14 | resources: 15 | limits: 16 | cpus: "0.1" 17 | memory: 50M 18 | labels: 19 | com.docker.lb.hosts: seife.mud.de 20 | com.docker.lb.network: webnetsb 21 | com.docker.lb.port: 2020 22 | restart_policy: 23 | condition: on-failure 24 | ports: 25 | - "2020:5000" 26 | networks: 27 | - webnetsb 28 | networks: 29 | webnetsb: 30 | attachable: true 31 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_secret.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | # replace username/repo:tag with your name and image details 5 | image: myonara/webmud3:v0.0.36 6 | environment: 7 | NODE_ENV: 'production' 8 | TLS: 'true' 9 | TLS_KEY: '/run/secrets/privkey.pem' 10 | TLS_CERT: '/run/secrets/cert.pem' 11 | # command: --tls-cert=/run/secrets/cert.pem --tls-key=/run/secrets/privkey.pem 12 | deploy: 13 | replicas: 1 14 | resources: 15 | limits: 16 | cpus: "0.1" 17 | memory: 50M 18 | labels: 19 | com.docker.lb.hosts: www.unitopia.de 20 | com.docker.lb.network: webnet 21 | com.docker.lb.port: 2018 22 | com.docker.lb.ssl_passthrough: "true" 23 | restart_policy: 24 | condition: on-failure 25 | ports: 26 | - "2018:5000" 27 | networks: 28 | - webnet 29 | secrets: 30 | - source: www.unitopia.de.cert 31 | target: /run/secrets/cert.pem 32 | - source: www.unitopia.de.key 33 | target: /run/secrets/privkey.pem 34 | networks: 35 | webnet: 36 | driver: overlay 37 | attachable: true 38 | secrets: 39 | www.unitopia.de.cert: 40 | file: ../run/secrets/cert.pem 41 | www.unitopia.de.key: 42 | file: ../run/secrets/privkey.pem 43 | 44 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_test.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | image: myonara/webmud3:latest 5 | environment: 6 | NODE_ENV: 'production' 7 | SECRET_CONFIG: '/run/webmud3test.json' 8 | MUD_CONFIG: '/run/mud_config_unitopia_test.json' 9 | WEBMUD3_DISTRIBUTION_TYPE: 'unitopia-test' 10 | volumes: 11 | - "/UNItopia/ftpwww/webmud3/run:/run" 12 | deploy: 13 | replicas: 1 14 | resources: 15 | limits: 16 | cpus: "0.1" 17 | memory: 50M 18 | labels: 19 | com.docker.lb.hosts: www.unitopia.de 20 | com.docker.lb.network: webnet 21 | com.docker.lb.port: 2019 22 | restart_policy: 23 | condition: on-failure 24 | delay: 20s 25 | max_attempts: 2 26 | window: 3600s 27 | ports: 28 | - "2019:5000" 29 | networks: 30 | - webnet 31 | networks: 32 | webnet: 33 | attachable: true 34 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_test_neu.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | image: myonara/webmud3:unitopiatest 5 | environment: 6 | NODE_ENV: 'production' 7 | SECRET_CONFIG: '/run/webmud3test.json' 8 | MUD_CONFIG: '/run/mud_config_unitopia_test.json' 9 | WEBMUD3_DISTRIBUTION_TYPE: 'unitopia-test' 10 | volumes: 11 | - "/UNItopia/ftpwww/webmud3/run:/run" 12 | deploy: 13 | replicas: 1 14 | resources: 15 | limits: 16 | cpus: "0.1" 17 | memory: 50M 18 | labels: 19 | com.docker.lb.hosts: www.unitopia.de 20 | com.docker.lb.network: webnet 21 | com.docker.lb.port: 2019 22 | restart_policy: 23 | condition: on-failure 24 | delay: 20s 25 | max_attempts: 2 26 | window: 3600s 27 | ports: 28 | - "2019:5000" 29 | networks: 30 | - webnet 31 | networks: 32 | webnet: 33 | attachable: true 34 | -------------------------------------------------------------------------------- /dockerfiles/w3_docker_compose_with_apache.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | web: 4 | # replace username/repo:tag with your name and image details 5 | image: myonara/webmud3:v0.0.36 6 | environment: 7 | NODE_ENV: 'production' 8 | # command: --tls-cert=/run/secrets/cert.pem --tls-key=/run/secrets/privkey.pem 9 | deploy: 10 | replicas: 1 11 | resources: 12 | limits: 13 | cpus: "0.1" 14 | memory: 50M 15 | labels: 16 | com.docker.lb.hosts: www.unitopia.de 17 | com.docker.lb.network: webnet 18 | com.docker.lb.port: 2018 19 | restart_policy: 20 | condition: on-failure 21 | ports: 22 | - "2018:5000" 23 | networks: 24 | webnet: 25 | ipv4_address: 172.16.238.10 26 | apache: 27 | image: 'bitnami/apache:2.4' 28 | deploy: 29 | replicas: 1 30 | resources: 31 | limits: 32 | cpus: "0.1" 33 | memory: 50M 34 | ports: 35 | - '8443:8443' 36 | volumes: 37 | - 'apache_data:/bitnami' 38 | networks: 39 | webnet: 40 | ipv4_address: 172.16.238.11 41 | volumes: 42 | apache_data: 43 | networks: 44 | webnet: 45 | driver: overlay 46 | attachable: true 47 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | module.exports = { 3 | preset: 'jest-preset-angular', 4 | setupFilesAfterEnv: ['/setup-jest.ts'], 5 | }; -------------------------------------------------------------------------------- /frontend/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|eot|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webmud3/frontend", 3 | "version": "1.0.0-alpha", 4 | "description": "Webmud3 Frontend", 5 | "engines": { 6 | "node": "20.12.2" 7 | }, 8 | "scripts": { 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "ng": "ng", 11 | "start": "ng serve", 12 | "start:intranet": "ng serve --host 0.0.0.0", 13 | "build": "ng build", 14 | "build:prod": "ng build --configuration=production", 15 | "test": "ng test", 16 | "lint": "ng lint", 17 | "lint:fix": "ng lint --fix" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular-eslint/schematics": "~17.3.0", 22 | "@angular/animations": "~17.3.6", 23 | "@angular/cdk": "~17.3.6", 24 | "@angular/common": "~17.3.6", 25 | "@angular/compiler": "~17.3.6", 26 | "@angular/core": "~17.3.6", 27 | "@angular/forms": "~17.3.6", 28 | "@angular/platform-browser": "~17.3.6", 29 | "@angular/platform-browser-dynamic": "~17.3.6", 30 | "@angular/router": "~17.3.6", 31 | "@ngrx/store": "~17.2.0", 32 | "ace-builds": "~1.33.1", 33 | "angular2-uuid": "~1.1.1", 34 | "ngx-cookie-service": "~17.1.0", 35 | "ngx-device-detector": "~7.0.0", 36 | "normalize.css": "^8.0.1", 37 | "primeflex": "~3.3.1", 38 | "primeicons": "~6.0.1", 39 | "primeng": "~17.15.0", 40 | "rxjs": "~7.8.1", 41 | "socket.io-client": "~4.7.5", 42 | "tslib": "~2.6.2", 43 | "zone.js": "~0.14.4" 44 | }, 45 | "devDependencies": { 46 | "@angular-builders/jest": "~17.0.3", 47 | "@angular-devkit/build-angular": "~17.3.6", 48 | "@angular-eslint/builder": "~17.3.0", 49 | "@angular-eslint/eslint-plugin": "~17.3.0", 50 | "@angular-eslint/eslint-plugin-template": "~17.3.0", 51 | "@angular-eslint/template-parser": "~17.3.0", 52 | "@angular/cli": "~17.3.6", 53 | "@angular/compiler-cli": "~17.3.6", 54 | "@types/jest": "~29.5.12", 55 | "@types/node": "20.12.2", 56 | "@typescript-eslint/eslint-plugin": "~7.7.1", 57 | "@typescript-eslint/parser": "~7.7.1", 58 | "eslint": "~8.57.0", 59 | "eslint-plugin-import": "^2.29.1", 60 | "eslint-plugin-simple-import-sort": "~12.1.0", 61 | "jest": "~29.7.0", 62 | "jest-preset-angular": "~14.0.3", 63 | "prettier": "~3.2.5", 64 | "ts-node": "~10.9.2", 65 | "typescript": "~5.4.5" 66 | }, 67 | "optionalDependencies": { 68 | "@nx/nx-darwin-arm64": "16.5.1", 69 | "@nx/nx-darwin-x64": "16.5.1", 70 | "@nx/nx-linux-x64-gnu": "16.5.1", 71 | "@nx/nx-win32-x64-msvc": "16.5.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/app.component.scss -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener } from '@angular/core'; 2 | 3 | import { ServerConfigService } from './shared/server-config.service'; 4 | import { WindowService } from './shared/window.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | }) 11 | export class AppComponent { 12 | constructor( 13 | public wincfg: WindowService, 14 | public srvcfg: ServerConfigService, 15 | ) { 16 | this.onResize(); 17 | } 18 | 19 | OnMenuAction(event: string, winid: string, other: any) { 20 | console.debug('appComponent-OnMenuAction', event, winid); 21 | this.wincfg.OnMenuAction(event, winid, other); 22 | } 23 | 24 | @HostListener('window:resize', ['$event']) 25 | onResize(event?: undefined) { 26 | this.wincfg.setWindowsSize(window.innerHeight, window.innerWidth); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { APP_INITIALIZER, NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | // import { ServiceWorkerModule } from '@angular/service-worker'; 5 | // import { environment } from '../environments/environment'; 6 | import { CoreModule } from '@mudlet3/frontend/core'; 7 | import { GmcpModule } from '@mudlet3/frontend/features/gmcp'; 8 | import { MudconfigModule } from '@mudlet3/frontend/features/mudconfig'; 9 | import { SettingsModule } from '@mudlet3/frontend/features/settings'; 10 | import { WidgetsModule } from '@mudlet3/frontend/features/widgets'; 11 | import { CookieService } from 'ngx-cookie-service'; 12 | import { SharedModule } from 'primeng/api'; 13 | 14 | import { AppComponent } from './app.component'; 15 | import { MudConfigService } from './features/config/mud-config.service'; 16 | import { ModelessModule } from './features/modeless/modeless.module'; 17 | import { PrimeModule } from './shared/prime.module'; 18 | import { WINDOW_PROVIDERS } from './shared/WINDOW_PROVIDERS'; 19 | 20 | /* eslint @typescript-eslint/ban-types: "warn" */ 21 | export function setupAppConfigServiceFactory( 22 | service: MudConfigService, 23 | ): Function { 24 | // console.log("LOADING Config"); 25 | return () => service.load(); 26 | } 27 | 28 | const features = [ 29 | GmcpModule, 30 | ModelessModule, 31 | MudconfigModule, 32 | SettingsModule, 33 | WidgetsModule, 34 | ]; 35 | 36 | @NgModule({ 37 | declarations: [AppComponent], 38 | imports: [ 39 | PrimeModule, 40 | ...features, 41 | SharedModule, 42 | CoreModule, 43 | BrowserModule, 44 | HttpClientModule, 45 | 46 | // ServiceWorkerModule.register('ngsw-worker.js', { 47 | // enabled: environment.production, 48 | // registrationStrategy: 'registerImmediately' 49 | // }) 50 | ], 51 | providers: [ 52 | WINDOW_PROVIDERS, 53 | CookieService, 54 | { 55 | provide: APP_INITIALIZER, 56 | useFactory: setupAppConfigServiceFactory, 57 | deps: [MudConfigService], 58 | multi: true, 59 | }, 60 | ], 61 | bootstrap: [AppComponent], 62 | }) 63 | export class AppModule {} 64 | -------------------------------------------------------------------------------- /frontend/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MenuModule } from './menu/menu.module'; 4 | import { MudModule } from './mud/mud.module'; 5 | 6 | @NgModule({ 7 | imports: [MenuModule, MudModule], 8 | exports: [MenuModule, MudModule], 9 | }) 10 | export class CoreModule {} 11 | -------------------------------------------------------------------------------- /frontend/src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | /* Menu Core Module */ 2 | export { MenuService } from './menu/menu.service'; 3 | 4 | /* Mud Core Module */ 5 | export { CoreModule } from './core.module'; 6 | export { MudModule } from './mud/mud.module'; 7 | -------------------------------------------------------------------------------- /frontend/src/app/core/menu/menu.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { PrimeModule } from '@mudlet3/frontend/shared'; 4 | 5 | import { MudmenuComponent } from './mud-menu/mud-menu.component'; 6 | 7 | @NgModule({ 8 | declarations: [MudmenuComponent], 9 | imports: [CommonModule, PrimeModule], 10 | exports: [MudmenuComponent], 11 | }) 12 | export class MenuModule {} 13 | -------------------------------------------------------------------------------- /frontend/src/app/core/menu/mud-menu/mud-menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/app/core/menu/mud-menu/mud-menu.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/core/menu/mud-menu/mud-menu.component.scss -------------------------------------------------------------------------------- /frontend/src/app/core/menu/mud-menu/mud-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { MenuItemCommandEvent } from 'primeng/api'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { MenuService } from '../menu.service'; 6 | import { MenuState } from '../types/menu-state'; 7 | 8 | @Component({ 9 | selector: 'app-mud-menu', 10 | templateUrl: './mud-menu.component.html', 11 | styleUrls: ['./mud-menu.component.scss'], 12 | }) 13 | export class MudmenuComponent { 14 | public readonly menuState$: Observable; 15 | 16 | @Output() 17 | public disconnectClicked: EventEmitter; 18 | 19 | @Output() 20 | public connectClicked: EventEmitter; 21 | 22 | constructor(private menuService: MenuService) { 23 | this.menuState$ = this.menuService.menuState$; 24 | 25 | this.disconnectClicked = this.menuService.disconnectClicked; 26 | this.connectClicked = this.menuService.connectClicked; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/core/menu/types/menu-state.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem } from 'primeng/api'; 2 | 3 | export interface MenuState { 4 | menuType: MenuType; 5 | items: MenuItem[]; 6 | } 7 | 8 | export enum MenuType { 9 | OTHER = 'OTHER', 10 | MUD_CLIENT = 'MUD_CLIENT', 11 | EDITOR = 'EDITOR', 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-input/mud-input.component.html: -------------------------------------------------------------------------------- 1 |
2 | 14 | 22 |
23 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-input/mud-input.component.scss: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1 1 auto; 5 | 6 | .mud-input { 7 | font-family: monospace; 8 | font-size: 14px; 9 | flex: 1 1 auto; 10 | resize: none; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-output/mud-output.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-output/mud-output.component.scss: -------------------------------------------------------------------------------- 1 | // .above-scroller { 2 | // overflow-x: scroll; 3 | // overflow-y: hidden; 4 | // display: block; 5 | // height: 20px; 6 | // } 7 | 8 | // .scroller { 9 | // height: 20px; 10 | // } 11 | 12 | :host { 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .mud-output { 18 | flex: 0 1 100%; 19 | 20 | overflow: auto; 21 | flex-direction: column; 22 | flex-wrap: wrap; 23 | font-family: monospace; 24 | font-size: 1rem; 25 | padding: 4px; 26 | 27 | .mud-message { 28 | display: contents; 29 | } 30 | } 31 | 32 | @media (max-width: 1000px) { 33 | .mud-output { 34 | font-size: 0.93rem; 35 | } 36 | } 37 | 38 | @media (max-width: 720px) { 39 | .mud-output { 40 | font-size: 0.75rem; 41 | } 42 | } 43 | 44 | @media (max-width: 580px) { 45 | .mud-output { 46 | font-size: 0.7rem; 47 | } 48 | } 49 | 50 | @media (max-width: 500px) { 51 | .mud-output { 52 | font-size: 0.6rem; 53 | } 54 | } 55 | 56 | @media (max-width: 450px) { 57 | .mud-output { 58 | font-size: 0.49rem; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-output/mud-output.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewChecked, 3 | AfterViewInit, 4 | Component, 5 | ElementRef, 6 | Input, 7 | ViewChild, 8 | } from '@angular/core'; 9 | import { BehaviorSubject, Observable } from 'rxjs'; 10 | 11 | import { IMudMessage } from '../../types/mud-message'; 12 | 13 | @Component({ 14 | selector: 'app-mud-output', 15 | templateUrl: './mud-output.component.html', 16 | styleUrls: ['./mud-output.component.scss'], 17 | }) 18 | export class MudOutputComponent implements AfterViewChecked, AfterViewInit { 19 | private readonly linesSubject = new BehaviorSubject([]); 20 | 21 | private canScrollToBottom = true; 22 | 23 | @ViewChild('container', { static: true }) 24 | private readonly outputContainer!: ElementRef; 25 | 26 | protected readonly lines$: Observable = 27 | this.linesSubject.asObservable(); 28 | 29 | public get lines(): IMudMessage[] { 30 | return this.linesSubject.value; 31 | } 32 | 33 | @Input({ required: true }) 34 | public set lines(value: IMudMessage[]) { 35 | this.linesSubject.next(value); 36 | } 37 | 38 | @Input({ required: true }) 39 | public foregroundColor!: string; 40 | 41 | @Input({ required: true }) 42 | public backgroundColor!: string; 43 | 44 | public ngAfterViewChecked() { 45 | if (this.canScrollToBottom) { 46 | this.scrollToBottom(); 47 | } 48 | } 49 | 50 | public ngAfterViewInit(): void { 51 | this.outputContainer.nativeElement.onscroll = (event: Event) => { 52 | this.onScroll(event); 53 | }; 54 | } 55 | 56 | private onScroll(event: Event): void { 57 | const element = event.target as HTMLElement; 58 | const tolerance = 5; 59 | const atBottom = 60 | Math.abs( 61 | element.scrollHeight - element.scrollTop - element.clientHeight, 62 | ) <= tolerance; 63 | 64 | this.canScrollToBottom = atBottom; 65 | } 66 | 67 | private scrollToBottom(): void { 68 | this.outputContainer.nativeElement.scrollTop = 69 | this.outputContainer.nativeElement.scrollHeight; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-span/mud-span.component.html: -------------------------------------------------------------------------------- 1 | {{ mudLine.text }} 8 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/components/mud-span/mud-span.component.scss: -------------------------------------------------------------------------------- 1 | span { 2 | white-space: pre; 3 | } 4 | .bold { 5 | font-weight: bold; 6 | } 7 | .italic { 8 | font-style: italic; 9 | } 10 | .underline { 11 | text-decoration: underline; 12 | } 13 | .crossedout { 14 | text-decoration: line-through; 15 | } 16 | .faint { 17 | filter: brightness(45%); 18 | } 19 | .blink { 20 | animation: blink-animation 1s steps(5, start) infinite; 21 | -webkit-animation: blink-animation 1s steps(5, start) infinite; 22 | } 23 | @keyframes blink-animation { 24 | to { 25 | visibility: hidden; 26 | } 27 | } 28 | @-webkit-keyframes blink-animation { 29 | to { 30 | visibility: hidden; 31 | } 32 | } 33 | 34 | 35 | /* Tooltip container */ 36 | .tooltip { 37 | position: relative; 38 | display: inline-block; 39 | border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ 40 | } 41 | 42 | /* Tooltip text */ 43 | .tooltip .tooltiptext { 44 | visibility: hidden; 45 | width: 120px; 46 | background-color: black; 47 | color: #fff; 48 | text-align: center; 49 | padding: 5px 0; 50 | border-radius: 6px; 51 | 52 | /* Position the tooltip text - see examples below! */ 53 | position: absolute; 54 | z-index: 1; 55 | width: 120px; 56 | bottom: 100%; 57 | left: 50%; 58 | margin-left: -60px; /* Use half of the width (120/2 = 60), to center the tooltip */ 59 | } 60 | 61 | /* Show the tooltip text when you mouse over the tooltip container */ 62 | .tooltip:hover .tooltiptext { 63 | visibility: visible; 64 | } -------------------------------------------------------------------------------- /frontend/src/app/core/mud/mud-client/mud-client.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 |
17 |

Disconnected!

18 | 19 |
20 |
21 | 22 | 24 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/mud-client/mud-client.component.scss: -------------------------------------------------------------------------------- 1 | // table { 2 | // width: 100%; 3 | // } 4 | 5 | // .mud-test { 6 | // font-family: monospace; 7 | // font-size: 14px; 8 | // background-color: black; 9 | // color: white; 10 | // padding: 0.5ex; 11 | // margin-top: 0; 12 | // margin-bottom: 0; 13 | // display: inline-block; 14 | // } 15 | :host { 16 | display: flex; 17 | flex-direction: column; 18 | flex: 1 0 100%; 19 | overflow: hidden; 20 | 21 | app-mud-menu { 22 | flex: 0 1 auto; 23 | } 24 | 25 | app-mud-output { 26 | flex: 1 1 auto; 27 | overflow-y: auto; 28 | } 29 | 30 | app-mud-input { 31 | flex: 0 0 auto; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | #disconnected-panel { 37 | display: flex; 38 | gap: 32px; 39 | padding: 4px; 40 | align-items: center; 41 | align-self: center; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/mud.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { WidgetsModule } from '@mudlet3/frontend/features/widgets'; 7 | import { PrimeModule } from '@mudlet3/frontend/shared'; 8 | 9 | import { MenuModule } from '../menu/menu.module'; 10 | import { MudInputComponent } from './components/mud-input/mud-input.component'; 11 | import { MudOutputComponent } from './components/mud-output/mud-output.component'; 12 | import { MudspanComponent } from './components/mud-span/mud-span.component'; 13 | import { MudclientComponent } from './mud-client/mud-client.component'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | MudclientComponent, 18 | MudspanComponent, 19 | MudInputComponent, 20 | MudOutputComponent, 21 | ], 22 | imports: [ 23 | CommonModule, 24 | BrowserModule, 25 | PrimeModule, 26 | BrowserAnimationsModule, 27 | FormsModule, 28 | ReactiveFormsModule, 29 | WidgetsModule, 30 | MenuModule, 31 | ], 32 | exports: [MudclientComponent], 33 | }) 34 | export class MudModule {} 35 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/mud.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MudConfigService } from '@mudlet3/frontend/features/config'; 3 | import { SocketsService } from '@mudlet3/frontend/features/sockets'; 4 | import { 5 | wordWrap, 6 | isSecureString, 7 | SecureString, 8 | } from '@mudlet3/frontend/shared'; 9 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; 10 | import { IMudMessage } from './types/mud-message'; 11 | 12 | import { mudProcessData } from './utils/mud-process-data'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class MudService { 18 | private readonly outputLines = new BehaviorSubject([]); 19 | 20 | private readonly newMessageToSend: Subject = new Subject(); 21 | 22 | public readonly outputLines$: Observable = 23 | this.outputLines.asObservable(); 24 | 25 | public readonly connectedToMud$: Observable; 26 | 27 | public readonly showEcho$: Observable; 28 | 29 | constructor( 30 | private readonly socketsService: SocketsService, 31 | private readonly mudConfigService: MudConfigService, 32 | ) { 33 | socketsService.onMudOutput.subscribe(({ data }) => { 34 | const ansiData = mudProcessData(data); 35 | 36 | const mudLines: IMudMessage[] = ansiData.map((ansi) => ({ 37 | ...ansi, 38 | type: 'mud', 39 | })); 40 | 41 | this.addOutputLine(...mudLines); 42 | }); 43 | 44 | this.connectedToMud$ = this.socketsService.connectedToMud$; 45 | 46 | this.showEcho$ = socketsService.onSetEchoMode.asObservable(); 47 | } 48 | 49 | public addOutputLine(...line: IMudMessage[]): void { 50 | this.outputLines.next([...this.outputLines.value, ...line]); 51 | } 52 | 53 | public sendMessage(message: string | SecureString): void { 54 | this.socketsService.sendMessage(message); 55 | 56 | const isSecure = isSecureString(message); 57 | 58 | const useEcho = this.mudConfigService.webConfig.localEcho; 59 | 60 | if (useEcho && !isSecure) { 61 | const echoLine: IMudMessage = { 62 | type: 'echo', 63 | // Todo[myst]: die Anzahl der Zeichen sollte mit dem Ausgehandelten WordWrap von Uni übereinstimmen 64 | text: wordWrap(message, 75) + '\r\n', 65 | }; 66 | 67 | this.addOutputLine(echoLine); 68 | } 69 | } 70 | 71 | public connect(): void { 72 | this.socketsService.connectToMud(); 73 | } 74 | 75 | public disconnect(): void { 76 | this.socketsService.disconnectFromMud(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/types/mud-message.ts: -------------------------------------------------------------------------------- 1 | export interface IMudMessage { 2 | text: string; 3 | type: 'mud' | 'echo' | 'system'; 4 | bold?: boolean; 5 | faint?: boolean; 6 | italic?: boolean; 7 | underline?: boolean; 8 | blink?: boolean; 9 | crossedout?: boolean; 10 | fgcolor?: string; 11 | bgcolor?: string; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/types/mud-signal-handler-data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacterData, 3 | MudSignals, 4 | WindowConfig, 5 | } from '@mudlet3/frontend/shared'; 6 | 7 | import { IAnsiData } from '@mudlet3/frontend/features/ansi'; 8 | import { IMudMessage } from './mud-message'; 9 | 10 | // Todo[myst] very bad interface since it is the mudemenu.component. Use composition over inheritance. 11 | export interface MudSignalHandlerData { 12 | v: { inpType: string }; 13 | titleService: { setTitle: (title: string) => void }; 14 | charData: CharacterData; 15 | filesrv: { 16 | startFilesModule: () => void; 17 | processFileInfo: (fileInfo: any) => any; 18 | }; 19 | wincfg: { 20 | SavedAndClose: (windowId: any) => void; 21 | WinError: (windowId: any, error: any) => void; 22 | SaveComplete: (windowId: any, closable: boolean) => void; 23 | newWindow: (config: WindowConfig) => string; 24 | findFilesWindow: (filesWindow: any, musi: MudSignals) => any; 25 | }; 26 | invlist: { 27 | initList: (entries: any) => void; 28 | addItem: (entry: any) => void; 29 | removeItem: (entry: any) => void; 30 | }; 31 | socketsService: { 32 | sendGMCP: (id: string, module: string, command: string, data: any) => void; 33 | }; 34 | keySetters: { setLevel: (level: any) => void }; 35 | messages: IMudMessage[]; 36 | ansiCurrent: IAnsiData; 37 | togglePing: boolean; 38 | inpmessage: string; 39 | filesWindow: any; 40 | doFocus: () => void; 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/utils/do-focus.ts: -------------------------------------------------------------------------------- 1 | import { ElementRef } from '@angular/core'; 2 | 3 | export function doFocus( 4 | v: any, 5 | changeFocus: number, 6 | previousFoxus: number, 7 | mudInputLine?: ElementRef, 8 | mudInputArea?: ElementRef, 9 | ) { 10 | let FirstFocus = undefined; 11 | previousFoxus = changeFocus; 12 | if (v.inpType != 'text' && typeof mudInputLine !== 'undefined') { 13 | FirstFocus = mudInputLine.nativeElement; 14 | changeFocus = 1; 15 | } else if (v.inpType == 'text' && typeof mudInputArea !== 'undefined') { 16 | FirstFocus = mudInputArea.nativeElement; 17 | } else if (v.inpType != 'text') { 18 | changeFocus = 2; 19 | return { changeFocus, previousFoxus }; 20 | } else if (v.inpType == 'text') { 21 | changeFocus = -2; 22 | return { changeFocus, previousFoxus }; 23 | } 24 | if (FirstFocus) { 25 | FirstFocus.focus(); 26 | FirstFocus.select(); 27 | } 28 | return { changeFocus, previousFoxus }; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/utils/mud-process-data.ts: -------------------------------------------------------------------------------- 1 | import { IAnsiData, processAnsiData } from '@mudlet3/frontend/features/ansi'; 2 | import { WithRequired } from '@mudlet3/frontend/shared'; 3 | 4 | export function mudProcessData( 5 | data: string, 6 | ): WithRequired, 'text'>[] { 7 | // if (typeof outp !== 'undefined') { 8 | // const idx = outp?.indexOf(ESCAPE_SEQUENCES.CLEAR_SCREEN); 9 | 10 | // if (idx !== undefined && idx >= 0) { 11 | // data.messages = []; 12 | // data.mudlines = []; 13 | // } 14 | // data.ansiCurrent.ansi = outp; 15 | // data.ansiCurrent.mudEcho = undefined; 16 | // data.messages.push({ text: outp }); 17 | // } else { 18 | // data.ansiCurrent.ansi = ''; 19 | // data.ansiCurrent.mudEcho = iecho; 20 | // data.messages.push({ text: iecho }); 21 | // } 22 | 23 | // const ts = new Date(); 24 | // data.ansiCurrent.timeString = 25 | // (ts.getDate() < 10 ? '0' : '') + 26 | // ts.getDate() + 27 | // '.' + 28 | // (ts.getMonth() + 1 < 10 ? '0' : '') + 29 | // (ts.getMonth() + 1) + 30 | // '.' + 31 | // ts.getFullYear() + 32 | // ' ' + 33 | // (ts.getHours() < 10 ? '0' : '') + 34 | // ts.getHours() + 35 | // ':' + 36 | // (ts.getMinutes() < 10 ? '0' : '') + 37 | // ts.getMinutes() + 38 | // ':' + 39 | // (ts.getSeconds() < 10 ? '0' : '') + 40 | // ts.getSeconds(); 41 | 42 | return processAnsiData(data); 43 | 44 | // Todo: Hier wurden die Mudlines zusammengebaselt 45 | 46 | // for (let ix = 0; ix < a2harr.length; ix++) { 47 | // if (a2harr[ix].text != '' || typeof a2harr[ix].mudEcho !== 'undefined') { 48 | // data.mudlines = data.mudlines.concat(a2harr[ix]); 49 | // } 50 | // } 51 | 52 | // data.ansiCurrent = a2harr[a2harr.length - 1]; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | import { ElementRef } from '@angular/core'; 2 | 3 | export function scroll(mudBlock?: ElementRef, scroller?: ElementRef) { 4 | mudBlock?.nativeElement.scrollTo(scroller?.nativeElement.scrollLeft, 0); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/app/core/mud/utils/table-output.ts: -------------------------------------------------------------------------------- 1 | export function tableOutput(words: string[], screen: number): string { 2 | let width = 1; 3 | words.forEach((word) => { 4 | if (word.length > width) { 5 | width = word.length; 6 | } 7 | }); 8 | width++; 9 | const cols = Math.max(1, Math.floor((screen + 1) / (width + 1))); 10 | const lines = Math.floor((words.length + cols - 1) / cols); 11 | width = Math.max(width + 1, Math.floor((screen + 1) / cols)); 12 | const r: string[] = []; 13 | for (let line = 0; line < lines; line++) { 14 | let s = ''; 15 | const colMin = Math.min( 16 | cols, 17 | Math.floor((words.length - line + lines - 1) / lines), 18 | ); 19 | for (let col = 0; col < colMin; col++) { 20 | const word = words[line + col * lines]; 21 | const len = width - word.length; 22 | s += word + ' '.repeat(len); 23 | } 24 | r.push(s); 25 | } 26 | return '\r\n' + r.join('\r\n'); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/index.ts: -------------------------------------------------------------------------------- 1 | export { ESCAPE_SEQUENCES } from './models/escape-sequences'; 2 | export { IAnsiData } from './types/ansi-data'; 3 | export { invColor } from './utils/colors/inv-color'; 4 | export { invertGrayscale } from './utils/colors/invert-grayscale'; 5 | export { decodeBinaryBase64 } from './utils/converter/decode-binary-base64'; 6 | export { encodeBinaryBase64 } from './utils/converter/encode-binary-base64'; 7 | export { processAnsiData } from './utils/process-ansi-data'; 8 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/models/default-formatting-data.ts: -------------------------------------------------------------------------------- 1 | import { FormatData } from '../types/format-data'; 2 | 3 | export const DefaultFormatData: FormatData = { 4 | fgcolor: '#ffffff', 5 | bgcolor: '#000000', 6 | bold: false, 7 | faint: false, 8 | blink: false, 9 | italic: false, 10 | underline: false, 11 | reverse: false, 12 | concealed: false, 13 | crossedout: false, 14 | } as const; 15 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/models/escape-sequences.ts: -------------------------------------------------------------------------------- 1 | export const ESCAPE_SEQUENCES = { 2 | CLEAR_SCREEN: 'e[He[J', 3 | FG256: '38;5;', 4 | BG256: '48;5;', 5 | FG_RGB: '39;2;', 6 | BG_RGB: '49;2;', 7 | VALID: /^[0-9;A-Za-z]$/, 8 | ENDCHAR: /^[A-Za-z]$/, 9 | } as const; 10 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/types/ansi-data.ts: -------------------------------------------------------------------------------- 1 | import { FormatData } from './format-data'; 2 | 3 | // Todo[myst]: The object from this thing is used und reasigned everywhere. Make props readonly and see what breaks 4 | export interface IAnsiData extends FormatData { 5 | // ansi: string; 6 | // mudEcho?: string; 7 | // optionInvert: boolean; 8 | // fontheight: number; 9 | // fontwidth: number; 10 | // ansiPos: number; 11 | // lastEscape?: string; 12 | // timeString: string; 13 | text: string; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/types/format-data.ts: -------------------------------------------------------------------------------- 1 | export type FormatData = { 2 | bold: boolean; 3 | faint: boolean; 4 | italic: boolean; 5 | underline: boolean; 6 | blink: boolean; 7 | reverse: boolean; 8 | concealed: boolean; 9 | crossedout: boolean; 10 | fgcolor: string; 11 | bgcolor: string; 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/convert-rgb-to-hex.spec.ts: -------------------------------------------------------------------------------- 1 | import { convertRgbToHex } from './convert-rgb-to-hex'; 2 | 3 | describe('convertRgbToHex', () => { 4 | it('should convert RGB array to hex string', () => { 5 | expect(convertRgbToHex(['255', '0', '0'])).toBe('#ff0000'); 6 | expect(convertRgbToHex(['0', '255', '0'])).toBe('#00ff00'); 7 | expect(convertRgbToHex(['0', '0', '255'])).toBe('#0000ff'); 8 | }); 9 | 10 | it('should handle invalid RGB arrays', () => { 11 | expect(() => 12 | convertRgbToHex(['255', '0'] as unknown as [string, string, string]), 13 | ).toThrow('Invalid RGB array'); 14 | expect(() => convertRgbToHex(['255', '0', '256'])).toThrow( 15 | 'Invalid RGB array', 16 | ); 17 | expect(() => convertRgbToHex(['255', '0', '-1'])).toThrow( 18 | 'Invalid RGB array', 19 | ); 20 | expect(() => convertRgbToHex(['255', '0', 'abc'])).toThrow( 21 | 'Invalid RGB array', 22 | ); 23 | }); 24 | 25 | it('should handle edge cases', () => { 26 | expect(convertRgbToHex(['0', '0', '0'])).toBe('#000000'); 27 | expect(convertRgbToHex(['255', '255', '255'])).toBe('#ffffff'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/convert-rgb-to-hex.ts: -------------------------------------------------------------------------------- 1 | import { formatToHex } from '../converter/format-to-hex'; 2 | 3 | /** 4 | * Converts an array of RGB values to a hexadecimal color string. 5 | * 6 | * This function takes an array of three RGB values (as strings) and converts them 7 | * to a single hexadecimal color string. 8 | * 9 | * @param {[string, string, string]} rgb - An array of three strings representing RGB values. 10 | * @returns {string} - The hexadecimal color string. 11 | * 12 | * @throws {Error} - If the input array does not contain exactly three values or if the values are not valid numbers. 13 | * 14 | * @example 15 | * convertRgbToHex(['255', '0', '0']); // returns '#ff0000' 16 | * convertRgbToHex(['0', '255', '0']); // returns '#00ff00' 17 | */ 18 | export function convertRgbToHex(rgb: [string, string, string]): string { 19 | if ( 20 | rgb.length !== 3 || 21 | !rgb.every( 22 | (val) => 23 | !isNaN(parseInt(val)) && parseInt(val) >= 0 && parseInt(val) <= 255, 24 | ) 25 | ) { 26 | throw new Error('Invalid RGB array'); 27 | } 28 | 29 | const hexString = rgb.map((val) => formatToHex(parseInt(val))).join(''); 30 | 31 | return `#${hexString}`; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/extract-colors.ts: -------------------------------------------------------------------------------- 1 | import { invColor } from './inv-color'; 2 | import { invertGrayscale } from './invert-grayscale'; 3 | 4 | // Todo[myst]: Funktion refactoren bzw. Überladungen bereitstellen - einige Funktionspfade sind sinnlos (siehe auskommentierte tests) 5 | 6 | /** 7 | * Extracts and potentially inverts foreground and background colors based on various conditions. 8 | * 9 | * @param a2h - The ANSI data object containing foreground and background colors. 10 | * @param {boolean} bow - A flag indicating whether to invert grayscale colors. 11 | * @param {boolean} invert - A flag indicating whether to invert the colors. 12 | * @param {boolean} colorOff - A flag indicating whether to turn off color adjustments. 13 | * @param {[string, string]} colors - An optional tuple like array containing two color strings. 14 | * @returns {[string, string]} - An tuple like array containing the foreground and background colors. 15 | * @throws {Error} - If the input colors or a2h are invalid. 16 | * 17 | * @example 18 | * extractColors(a2h, ['#123456', '#654321'], false, true, false); // returns ['#edcba9', '#9abced'] 19 | */ 20 | export function extractColors( 21 | a2h: { reverse: boolean; bgcolor: string; fgcolor: string }, 22 | bow: boolean, 23 | invert: boolean, 24 | colorOff: boolean, 25 | colors?: [string, string], 26 | ): [string, string] { 27 | // Default colors based on colorOff, bow, and invert flags 28 | if (colorOff) { 29 | return bow || invert ? ['#000000', '#ffffff'] : ['#ffffff', '#000000']; 30 | } 31 | 32 | // Determine foreground and background colors 33 | const lfg: string = 34 | colors !== undefined && colors.length === 2 35 | ? colors[0] 36 | : a2h.reverse 37 | ? a2h.bgcolor 38 | : a2h.fgcolor; 39 | 40 | const lbg: string = 41 | colors !== undefined && colors.length === 2 42 | ? colors[1] 43 | : a2h.reverse 44 | ? a2h.fgcolor 45 | : a2h.bgcolor; 46 | 47 | // Apply inversion if needed 48 | const fgColor = invert ? invColor(lfg) : lfg; 49 | const bgColor = invert ? invColor(lbg) : lbg; 50 | 51 | // Return the result based on the bow flag 52 | return bow 53 | ? [invertGrayscale(fgColor), invertGrayscale(bgColor)] 54 | : [fgColor, bgColor]; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/inv-color.spec.ts: -------------------------------------------------------------------------------- 1 | import { invColor } from './inv-color'; 2 | 3 | describe('invColor', () => { 4 | it('should invert black to white', () => { 5 | expect(invColor('#000000')).toBe('#ffffff'); 6 | }); 7 | 8 | it('should invert white to black', () => { 9 | expect(invColor('#ffffff')).toBe('#000000'); 10 | }); 11 | 12 | it('should invert #123456 to #edcba9', () => { 13 | expect(invColor('#123456')).toBe('#edcba9'); 14 | }); 15 | 16 | it('should invert #abcdef to #543210', () => { 17 | expect(invColor('#abcdef')).toBe('#543210'); 18 | }); 19 | 20 | it('should invert #0000ff to #ffff00', () => { 21 | expect(invColor('#0000ff')).toBe('#ffff00'); 22 | }); 23 | 24 | // Additional tests for edge cases and error handling 25 | it('should handle short hex codes by expanding them correctly', () => { 26 | expect(invColor('#abc')).toBe('#554433'); 27 | }); 28 | 29 | it('should throw an error for invalid hex codes', () => { 30 | expect(() => invColor('#zzzzzz')).toThrow( 31 | 'Invalid hexadecimal color string', 32 | ); 33 | }); 34 | 35 | it('should throw an error for input without #', () => { 36 | expect(() => invColor('123456')).toThrow( 37 | 'Invalid hexadecimal color string', 38 | ); 39 | }); 40 | 41 | it('should throw an error for empty input', () => { 42 | expect(() => invColor('')).toThrow('Invalid hexadecimal color string'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/inv-color.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inverts the color provided as a hexadecimal string. 3 | * 4 | * This function takes a hexadecimal color string (e.g., '#ffffff' for white) 5 | * and returns its inverted color. The inversion is done by bitwise negating 6 | * the RGB values. 7 | * 8 | * @param {string} hex - The hexadecimal color string to invert. Must be in the format '#RRGGBB' or '#RGB'. 9 | * @returns {string} - The inverted hexadecimal color string in the format '#RRGGBB'. 10 | * @throws {Error} - If the input is not a valid hexadecimal color string. 11 | * 12 | * @example 13 | * invColor('#000000'); // returns '#ffffff' 14 | * invColor('#ffffff'); // returns '#000000' 15 | * invColor('#123456'); // returns '#edcba9' 16 | */ 17 | export function invColor(hex: string): string { 18 | if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) { 19 | throw new Error('Invalid hexadecimal color string'); 20 | } 21 | 22 | // Expand shorthand hex code to full form (e.g., #abc to #aabbcc) 23 | if (hex.length === 4) { 24 | hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; 25 | } 26 | 27 | // Convert hex to integer 28 | const num = parseInt(hex.slice(1), 16); 29 | 30 | // Invert the RGB values 31 | const invertedNum = 0xffffff ^ num; 32 | 33 | // Convert back to hex and ensure it is padded with leading zeros if necessary 34 | const invertedHex = invertedNum.toString(16).padStart(6, '0'); 35 | 36 | return `#${invertedHex}`; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/invert-grayscale.spec.ts: -------------------------------------------------------------------------------- 1 | import { invColor } from './inv-color'; 2 | import { invertGrayscale } from './invert-grayscale'; 3 | 4 | jest.mock('./inv-color'); 5 | 6 | describe('invertGrayscale', () => { 7 | beforeEach(() => { 8 | (invColor as jest.Mock).mockClear(); 9 | }); 10 | 11 | it('should invert black to white', () => { 12 | (invColor as jest.Mock).mockImplementation(() => '#ffffff'); 13 | expect(invertGrayscale('#000000')).toBe('#ffffff'); 14 | expect(invColor).toHaveBeenCalledWith('#000000'); 15 | }); 16 | 17 | it('should invert white to black', () => { 18 | (invColor as jest.Mock).mockImplementation(() => '#000000'); 19 | expect(invertGrayscale('#ffffff')).toBe('#000000'); 20 | expect(invColor).toHaveBeenCalledWith('#ffffff'); 21 | }); 22 | 23 | it('should return the original color if it is not grayscale', () => { 24 | expect(invertGrayscale('#123456')).toBe('#123456'); 25 | expect(invColor).not.toHaveBeenCalled(); 26 | }); 27 | 28 | it('should invert #333333 to #cccccc', () => { 29 | (invColor as jest.Mock).mockImplementation(() => '#cccccc'); 30 | expect(invertGrayscale('#333333')).toBe('#cccccc'); 31 | expect(invColor).toHaveBeenCalledWith('#333333'); 32 | }); 33 | 34 | it('should throw an error for invalid hex codes', () => { 35 | expect(() => invertGrayscale('#zzzzzz')).toThrow( 36 | 'Invalid hexadecimal color string', 37 | ); 38 | }); 39 | 40 | it('should throw an error for short hex codes', () => { 41 | expect(() => invertGrayscale('#abc')).toThrow( 42 | 'Invalid hexadecimal color string', 43 | ); 44 | }); 45 | 46 | it('should throw an error for input without #', () => { 47 | expect(() => invertGrayscale('123456')).toThrow( 48 | 'Invalid hexadecimal color string', 49 | ); 50 | }); 51 | 52 | it('should throw an error for empty input', () => { 53 | expect(() => invertGrayscale('')).toThrow( 54 | 'Invalid hexadecimal color string', 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/colors/invert-grayscale.ts: -------------------------------------------------------------------------------- 1 | import { invColor } from './inv-color'; 2 | 3 | /** 4 | * Converts a grayscale color (where R, G, and B are equal) to its inverse. 5 | * 6 | * This function checks if the provided hexadecimal color string is a grayscale 7 | * color (i.e., R, G, and B values are the same). If it is, it returns the 8 | * inverse color. Otherwise, it returns the original color. 9 | * 10 | * @param {string} hex - The hexadecimal color string to check and potentially invert. Must be in the format '#RRGGBB'. 11 | * @returns {string} - The original hexadecimal color string or its inverse if it is a grayscale color. 12 | * @throws {Error} - If the input is not a valid hexadecimal color string. 13 | * 14 | * @example 15 | * invertGrayscale('#000000'); // returns '#ffffff' 16 | * invertGrayscale('#ffffff'); // returns '#000000' 17 | * invertGrayscale('#123456'); // returns '#123456' 18 | * invertGrayscale('#333333'); // returns '#cccccc' 19 | */ 20 | export function invertGrayscale(hex: string): string { 21 | if (!/^#([0-9a-f]{6})$/i.test(hex)) { 22 | throw new Error('Invalid hexadecimal color string'); 23 | } 24 | 25 | // Convert hex to integer 26 | const num = parseInt(hex.slice(1), 16); 27 | 28 | // Extract RGB components 29 | const r = (num & 0xff0000) >> 16; 30 | const g = (num & 0x00ff00) >> 8; 31 | const b = num & 0x0000ff; 32 | 33 | // Check if the color is grayscale (R, G, and B values are equal) 34 | return r === g && g === b ? invColor(hex) : hex; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/decode-binary-base64.spec.ts: -------------------------------------------------------------------------------- 1 | import { decodeBinaryBase64 } from './decode-binary-base64'; 2 | 3 | describe('decodeBinaryBase64', () => { 4 | it('should decode a Base64-encoded binary string to its original UTF-16 string', () => { 5 | expect(decodeBinaryBase64('aABlAGwAbABvACAAdwBvAHIAbABkAA==')).toBe( 6 | 'hello world', 7 | ); 8 | expect( 9 | decodeBinaryBase64('UwBvAG0AZQAgAG8AdABoAGUAcgAgAHMAdAByAGkAbgBnAA=='), 10 | ).toBe('Some other string'); 11 | }); 12 | 13 | it('should handle empty strings', () => { 14 | expect(decodeBinaryBase64('')).toBe(''); 15 | }); 16 | 17 | it('should handle special characters', () => { 18 | expect(decodeBinaryBase64('ACYP/jzYCN8=')).toBe('☀️🌈'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/decode-binary-base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decodes a Base64-encoded binary string to its original UTF-16 string representation. 3 | * 4 | * This function takes a Base64-encoded binary string and decodes it to its original 5 | * UTF-16 string representation. 6 | * 7 | * @param {string} encoded - The Base64-encoded binary string to decode. 8 | * @returns {string} - The decoded UTF-16 string. 9 | * 10 | * @example 11 | * decodeBinaryBase64('SGVsbG8gd29ybGQ='); // returns 'Hello world' 12 | */ 13 | export function decodeBinaryBase64(encoded: string): string { 14 | const binary = atob(encoded); 15 | let result = ''; 16 | for (let i = 0; i < binary.length; i += 2) { 17 | const code = binary.charCodeAt(i) | (binary.charCodeAt(i + 1) << 8); 18 | result += String.fromCharCode(code); 19 | } 20 | return result; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/encode-binary-base64.spec.ts: -------------------------------------------------------------------------------- 1 | import { encodeBinaryBase64 } from './encode-binary-base64'; 2 | 3 | describe('encodeBinaryBase64', () => { 4 | it('should encode a UTF-16 string to a Base64-encoded binary string', () => { 5 | expect(encodeBinaryBase64('hello world')).toBe( 6 | 'aABlAGwAbABvACAAdwBvAHIAbABkAA==', 7 | ); 8 | expect(encodeBinaryBase64('Some other string')).toBe( 9 | 'UwBvAG0AZQAgAG8AdABoAGUAcgAgAHMAdAByAGkAbgBnAA==', 10 | ); 11 | }); 12 | 13 | it('should handle empty strings', () => { 14 | expect(encodeBinaryBase64('')).toBe(''); 15 | }); 16 | 17 | it('should handle special characters', () => { 18 | expect(encodeBinaryBase64('☀️🌈')).toBe('ACYP/jzYCN8='); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/encode-binary-base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes a UTF-16 string to a Base64-encoded binary string representation. 3 | * 4 | * This function takes a UTF-16 string and encodes it to a Base64-encoded binary string. 5 | * 6 | * @param {string} str - The UTF-16 string to encode. 7 | * @returns {string} - The Base64-encoded binary string. 8 | * 9 | * @example 10 | * encodeBinaryBase64('Hello world'); // returns 'SGVsbG8gd29ybGQ=' 11 | */ 12 | export function encodeBinaryBase64(str: string): string { 13 | let binary = ''; 14 | for (let i = 0; i < str.length; i++) { 15 | const code = str.charCodeAt(i); 16 | binary += String.fromCharCode(code & 0xff, code >> 8); 17 | } 18 | return btoa(binary); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/format-to-hex.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatToHex } from './format-to-hex'; 2 | 3 | describe('formatToHex', () => { 4 | it('should convert single-digit numbers to two-digit hex strings', () => { 5 | expect(formatToHex(0)).toBe('00'); 6 | expect(formatToHex(1)).toBe('01'); 7 | expect(formatToHex(15)).toBe('0f'); 8 | }); 9 | 10 | it('should convert double-digit numbers to hex strings', () => { 11 | expect(formatToHex(16)).toBe('10'); 12 | expect(formatToHex(255)).toBe('ff'); 13 | }); 14 | 15 | it('should handle edge cases', () => { 16 | expect(formatToHex(0)).toBe('00'); 17 | expect(formatToHex(255)).toBe('ff'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/converter/format-to-hex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a number to a two-digit hexadecimal string. 3 | * 4 | * This function takes a number and converts it to a hexadecimal string. If the 5 | * resulting string has less than two characters, it pads the string with a 6 | * leading zero. 7 | * 8 | * @param {number} val - The number to convert to a two-digit hexadecimal string. 9 | * @returns {string} - The two-digit hexadecimal string. 10 | * 11 | * @example 12 | * formatToHex(5); // returns '05' 13 | * formatToHex(255); // returns 'ff' 14 | */ 15 | export function formatToHex(val: number): string { 16 | const result = val.toString(16); 17 | return result.length < 2 ? '0' + result : result; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/process-ansi-codes.spec.ts: -------------------------------------------------------------------------------- 1 | import { IAnsiData } from '../types/ansi-data'; 2 | import { applyAnsiAttributes } from './apply-ansi-attributes'; 3 | import { processAnsiCodes } from './process-ansi-codes'; 4 | 5 | jest.mock('./apply-ansi-attributes', () => ({ 6 | applyAnsiAttributes: jest.fn((codes) => { 7 | // Simulate attribute application for testing 8 | const result: Partial = {}; 9 | if (codes.includes('1')) result.bold = true; 10 | if (codes.includes('31')) result.fgcolor = '#ff0000'; 11 | return result; 12 | }), 13 | })); 14 | 15 | describe('processAnsiCodes', () => { 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('should return null for unsupported ANSI codes', () => { 21 | expect(processAnsiCodes('J')).toBeNull(); 22 | expect(processAnsiCodes('H')).toBeNull(); 23 | expect(processAnsiCodes('A')).toBeNull(); 24 | expect(processAnsiCodes('B')).toBeNull(); 25 | expect(processAnsiCodes('C')).toBeNull(); 26 | expect(processAnsiCodes('D')).toBeNull(); 27 | expect(processAnsiCodes('K')).toBeNull(); 28 | expect(processAnsiCodes('s')).toBeNull(); 29 | expect(processAnsiCodes('u')).toBeNull(); 30 | expect(processAnsiCodes('r')).toBeNull(); 31 | }); 32 | 33 | it('should apply attributes for m code', () => { 34 | const result = processAnsiCodes('1;31m'); 35 | expect(result).toEqual({ bold: true, fgcolor: '#ff0000' }); 36 | expect(applyAnsiAttributes).toHaveBeenCalledWith(['1', '31'], false); 37 | }); 38 | 39 | it('should log error for unsupported escape codes', () => { 40 | console.error = jest.fn(); 41 | processAnsiCodes('unsupported'); 42 | }); 43 | 44 | it('should handle empty escape string gracefully', () => { 45 | expect(processAnsiCodes('')).toBeNull(); 46 | }); 47 | 48 | it('should handle edge cases gracefully', () => { 49 | const result = processAnsiCodes('1;31;'); 50 | expect(result).toEqual(null); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/process-ansi-codes.ts: -------------------------------------------------------------------------------- 1 | import { FormatData } from '../types/format-data'; 2 | import { applyAnsiAttributes } from './apply-ansi-attributes'; 3 | 4 | /** 5 | * Processes ANSI escape codes and applies the corresponding attributes. 6 | * 7 | * This function processes ANSI escape codes and applies the corresponding 8 | * attributes to the ANSI data. It returns a partial IAnsiData object or null 9 | * if the escape code is unsupported. 10 | * 11 | **/ 12 | export function processAnsiCodes( 13 | escape: string, 14 | optionInvert: boolean = false, 15 | ): Partial | null { 16 | switch (escape[escape.length - 1]) { 17 | case 'J': 18 | case 'H': 19 | // Unsupported ANSI codes for this context 20 | break; 21 | case 'A': 22 | case 'B': 23 | case 'C': 24 | case 'D': 25 | case 'K': 26 | case 's': 27 | case 'u': 28 | case 'r': 29 | // Reset code, no changes to newData needed here 30 | break; 31 | case 'm': 32 | const codes = escape.substring(0, escape.length - 1).split(';'); 33 | 34 | return applyAnsiAttributes(codes, optionInvert); 35 | } 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/process-ansi-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { DefaultFormatData } from '../models/default-formatting-data'; 2 | import { IAnsiData } from '../types/ansi-data'; 3 | import { processAnsiData } from './process-ansi-data'; 4 | 5 | describe('processAnsiData', () => { 6 | afterEach(() => { 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | it('should work', () => { 11 | // zlpc return VT_FG_RED VT_BOLD "Hi" VT_NORM "\n" 12 | const input = '\x1b[31m\x1b[1mHi\x1b[0m\n'; 13 | 14 | const result = processAnsiData(input); 15 | 16 | const expected: Partial[] = [ 17 | { 18 | text: 'Hi', 19 | fgcolor: '#cd0000', 20 | bold: true, 21 | }, 22 | // Command 0 resets everthing to default 23 | { 24 | ...DefaultFormatData, 25 | text: '\n', 26 | }, 27 | ]; 28 | 29 | expect(result).toEqual(expected); 30 | }); 31 | 32 | it('should work with multiple lines', () => { 33 | const input = '\u001b[37m\u001b[31m\u001b[1mHi\u001b[0m\u001b[0m\r\n'; 34 | 35 | const result = processAnsiData(input); 36 | 37 | const expected: Partial[] = [ 38 | { 39 | text: 'Hi', 40 | fgcolor: '#cd0000', 41 | bold: true, 42 | }, 43 | // Command 0 resets everthing to default 44 | { 45 | ...DefaultFormatData, 46 | text: '\r\n', 47 | }, 48 | ]; 49 | 50 | expect(result).toEqual(expected); 51 | }); 52 | 53 | it('should work with simple strings', () => { 54 | const input = 'Der verwirrte Felag sagt: hallo?\r\n'; 55 | 56 | const result = processAnsiData(input); 57 | 58 | const expected: Partial[] = [ 59 | { 60 | text: 'Der verwirrte Felag sagt: hallo?\r\n', 61 | }, 62 | ]; 63 | 64 | expect(result).toEqual(expected); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/process-ansi-data.ts: -------------------------------------------------------------------------------- 1 | import { IAnsiData } from '@mudlet3/frontend/features/ansi'; 2 | import { WithRequired } from '@mudlet3/frontend/shared'; 3 | 4 | import { processAnsiSequences } from './process-ansi-sequences'; 5 | 6 | function isAnsiDataWithText( 7 | ansiData: Partial[], 8 | ): ansiData is WithRequired, 'text'>[] { 9 | return ansiData.every((data) => data.text !== undefined); 10 | } 11 | 12 | export function processAnsiData( 13 | text: string, 14 | ): WithRequired, 'text'>[] { 15 | const parts = text.split('\x1b'); 16 | 17 | if (parts.length === 1) { 18 | return [ 19 | { 20 | text: parts[0], 21 | }, 22 | ]; 23 | } 24 | 25 | const results = parts 26 | .filter((part) => part !== '') 27 | .map((part) => processAnsiSequences(part)) 28 | .filter((part) => part !== null) as Partial[]; 29 | 30 | const reducedUntilText = results.reduce[]>((acc, part) => { 31 | const lastText = acc[acc.length - 1]?.text; 32 | 33 | if (lastText === undefined) { 34 | const last = acc.pop(); 35 | 36 | const updated = { 37 | ...last, 38 | ...part, 39 | }; 40 | 41 | acc.push(updated); 42 | } else { 43 | acc.push(part); 44 | } 45 | 46 | return acc; 47 | }, []); 48 | 49 | // reducedUntilText should have an text property by now 50 | if (!isAnsiDataWithText(reducedUntilText)) { 51 | throw new Error('Ansi data is missing text property'); 52 | } 53 | 54 | return reducedUntilText; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/app/features/ansi/utils/process-ansi-sequences.spec.ts: -------------------------------------------------------------------------------- 1 | import { processAnsiCodes } from './process-ansi-codes'; 2 | import { processAnsiSequences } from './process-ansi-sequences'; 3 | 4 | // Mocking the dependencies 5 | jest.mock('./process-ansi-codes'); 6 | 7 | describe('processAnsiSequences', () => { 8 | afterEach(() => { 9 | jest.clearAllMocks(); 10 | }); 11 | 12 | it('should return the correct result for a single bracketed sequence', () => { 13 | const sequence = '[32m'; 14 | 15 | (processAnsiCodes as jest.Mock).mockReturnValue({ fgcolor: 'green' }); 16 | 17 | const result = processAnsiSequences(sequence); 18 | 19 | expect(result).toMatchObject({ 20 | fgcolor: 'green', 21 | }); 22 | expect(processAnsiCodes).toHaveBeenCalledWith('32m'); 23 | }); 24 | 25 | it('should return the correct result for multiple bracketed sequences', () => { 26 | (processAnsiCodes as jest.Mock).mockReturnValue({ 27 | fgcolor: 'green', 28 | bold: true, 29 | underline: true, 30 | }); 31 | 32 | const part1 = processAnsiSequences('[32m'); 33 | 34 | expect(processAnsiCodes).toHaveBeenCalledWith('32m'); 35 | 36 | const part2 = processAnsiSequences('[1mWithText'); 37 | 38 | expect(processAnsiCodes).toHaveBeenCalledWith('1m'); 39 | 40 | const part3 = processAnsiSequences('[24m'); 41 | 42 | expect(processAnsiCodes).toHaveBeenCalledWith('24m'); 43 | 44 | const result = { 45 | ...part1, 46 | ...part2, 47 | ...part3, 48 | }; 49 | 50 | expect(result).toMatchObject({ 51 | fgcolor: 'green', 52 | bold: true, 53 | underline: true, 54 | text: 'WithText', 55 | }); 56 | 57 | expect(processAnsiCodes).toHaveBeenCalledWith('1m'); 58 | }); 59 | 60 | it('should handle simple unformatted text', () => { 61 | const sequence = 'Simple Text'; 62 | 63 | const result = processAnsiSequences(sequence); 64 | 65 | expect(result).toMatchObject({ 66 | text: 'Simple Text', 67 | }); 68 | }); 69 | 70 | it('should handle complex, formatted text', () => { 71 | const sequence = '[23mSimple Text'; 72 | 73 | const result = processAnsiSequences(sequence); 74 | 75 | expect(result).toMatchObject({ 76 | text: 'Simple Text', 77 | fgcolor: 'green', 78 | }); 79 | }); 80 | 81 | it('should return null for unsupported sequences', () => { 82 | const sequence = '7'; 83 | 84 | const result = processAnsiSequences(sequence); 85 | 86 | expect(result).toBeNull(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /frontend/src/app/features/config/index.ts: -------------------------------------------------------------------------------- 1 | export { MudConfigService } from './mud-config.service'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/features/config/models/default-webmud-config.ts: -------------------------------------------------------------------------------- 1 | import { IWebmudConfig } from '../types/webmud-config'; 2 | 3 | export const DefaultWebmudConfig: IWebmudConfig = { 4 | mudname: 'unitopia', 5 | autoConnect: false, 6 | autoLogin: false, 7 | autoUser: '', 8 | autoToken: '', 9 | autoPw: '', 10 | localEcho: true, 11 | width: 0, 12 | height: 0, 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/app/features/config/models/mud_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope":"local", 3 | "href": "/", 4 | "mudfamilies": { 5 | "basistelnet": { 6 | "charset": "ascii", 7 | "MXP": false, 8 | "GMCP": false, 9 | "GMCP_Support": {} 10 | }, 11 | "unitopia": { 12 | "charset": "utf8", 13 | "MXP": true, 14 | "GMCP": true, 15 | "GMCP_Support": { 16 | "Sound": { 17 | "version": "1", 18 | "standard": true, 19 | "optional": false 20 | }, 21 | "Char": { 22 | "version": "1", 23 | "standard": true, 24 | "optional": false 25 | }, 26 | "Char.Items": { 27 | "version": "1", 28 | "standard": true, 29 | "optional": false 30 | }, 31 | "Comm": { 32 | "version": "1", 33 | "standard": true, 34 | "optional": false 35 | }, 36 | "Playermap": { 37 | "version": "1", 38 | "standard": false, 39 | "optional": true 40 | }, 41 | "Files": { 42 | "version": "1", 43 | "standard": true, 44 | "optional": false 45 | } 46 | } 47 | } 48 | }, 49 | "muds": { 50 | "unitopia": { 51 | "name": "UNItopia", 52 | "host": "unitopia.de", 53 | "port": 992, 54 | "ssl": true, 55 | "rejectUnauthorized": true, 56 | "description": "UNItopia via SSL", 57 | "playerlevel": "all", 58 | "mudfamily": "unitopia" 59 | }, 60 | "orbit": { 61 | "name": "Orbit", 62 | "host": "unitopia.de", 63 | "port": 9988, 64 | "ssl": true, 65 | "rejectUnauthorized": true, 66 | "description": "Orbit via SSL", 67 | "playerlevel": "wizard,testplayer", 68 | "mudfamily": "unitopia" 69 | }, 70 | "uni1993": { 71 | "name": "Uni1993", 72 | "host": "unitopia.de", 73 | "port": 1993, 74 | "ssl": false, 75 | "rejectUnauthorized": false, 76 | "description": "Unitopia 1993", 77 | "playerlevel": "all", 78 | "mudfamily": "basistelnet" 79 | }, 80 | "seifenblase": { 81 | "name": "Seifenblase", 82 | "host": "seifenblase.de", 83 | "port": 3333, 84 | "ssl": false, 85 | "rejectUnauthorized": false, 86 | "description": "Seifenblase", 87 | "playerlevel": "all", 88 | "mudfamily": "basistelnet" 89 | } 90 | }, 91 | "routes": { 92 | "/": "unitopia", 93 | "/unitopia": "unitopia", 94 | "/orbit": "orbit", 95 | "/uni1993": "uni1993", 96 | "/seifenblase": "seifenblase" 97 | } 98 | } -------------------------------------------------------------------------------- /frontend/src/app/features/config/mud-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { DefaultWebmudConfig } from './models/default-webmud-config'; 4 | import config from './models/mud_config.json'; 5 | import { IMudConfig } from './types/mud-config'; 6 | import { IWebmudConfig } from './types/webmud-config'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class MudConfigService { 12 | public readonly webConfig: IWebmudConfig = DefaultWebmudConfig; 13 | 14 | public data: IMudConfig = {}; 15 | 16 | load(): Promise { 17 | return new Promise((resolve) => { 18 | this.data = config; 19 | 20 | resolve(config); 21 | 22 | // this.http.get(getBaseLocation() + 'config/mud_config.json').subscribe( 23 | // (response) => { 24 | // // console.log('USING server-side configuration'); 25 | // this.data = Object.assign({}, defaults || {}, response || {}); 26 | // console.log('server-side-scope:', this.data); 27 | // resolve(this.data); 28 | // }, 29 | // () => { 30 | // // console.log('USING default configuration, scope local'); 31 | // this.data = Object.assign({}, defaults || {}); 32 | // console.log('default-scope:', this.data); 33 | // resolve(this.data); 34 | // }, 35 | // ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/features/config/types/mud-config.ts: -------------------------------------------------------------------------------- 1 | export interface IMudConfig { 2 | scope?: string; 3 | href?: string; 4 | mudfamilies?: any; 5 | muds?: any; 6 | routes?: any; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/features/config/types/webmud-config.ts: -------------------------------------------------------------------------------- 1 | export interface IWebmudConfig { 2 | mudname: string; 3 | autoConnect: boolean; 4 | autoLogin: boolean; 5 | autoUser: string; 6 | autoToken: string; 7 | autoPw?: string; 8 | localEcho: boolean; 9 | width: number; 10 | height: number; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/features/files/files.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { FileInfo } from '@mudlet3/frontend/shared'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class FilesService { 9 | private filemap: Record = {}; 10 | 11 | startFilesModule() { 12 | return; 13 | } 14 | 15 | processFileInfo(fileinfo: FileInfo): FileInfo { 16 | const url = fileinfo.lasturl; 17 | const filepath = fileinfo.file; 18 | console.debug('FilesService-processFileInfo-start', fileinfo); 19 | if ( 20 | Object.prototype.hasOwnProperty.call(this.filemap, filepath) && 21 | fileinfo.saveActive 22 | ) { 23 | const cfileinfo: FileInfo = this.filemap[filepath]; 24 | cfileinfo.save02_url?.(url); 25 | cfileinfo.alreadyLoaded = true; 26 | cfileinfo.saveActive = true; 27 | console.debug('FilesService-processFileInfo-alreadyLoaded', cfileinfo); 28 | return cfileinfo; 29 | } else { 30 | fileinfo.saveActive = false; 31 | fileinfo.alreadyLoaded = false; 32 | this.filemap[filepath] = fileinfo; 33 | } 34 | const other = this; 35 | fileinfo.relateWindow = function (winid: string) { 36 | console.debug('FilesService-relaeWindow', winid); 37 | this.windowsId = winid; 38 | other.filemap[filepath] = this; 39 | }; 40 | fileinfo.save02_url = function (url2) { 41 | fileinfo.lasturl = url2; 42 | console.debug('FilesService-save02_url', fileinfo); 43 | other.http 44 | .put(url2, fileinfo.content, { responseType: 'text' }) 45 | .subscribe( 46 | (value: string) => { 47 | fileinfo.oldContent = fileinfo.content; 48 | fileinfo.saveActive = false; 49 | fileinfo.save03_saved?.(filepath); 50 | }, 51 | (err: any) => { 52 | console.error('FilesService-save02_url-rrror', fileinfo, err); 53 | 54 | if (fileinfo.windowsId !== undefined) { 55 | fileinfo.save05_error?.(fileinfo.windowsId, err); 56 | } 57 | }, 58 | ); 59 | }; 60 | fileinfo.load = function (cb: Function) { 61 | other.http.get(url, { responseType: 'text' }).subscribe( 62 | (value: string) => { 63 | console.debug('FilesService-load', filepath); 64 | cb(undefined, value); 65 | }, 66 | (err: any) => { 67 | console.debug('FilesService-load-failed', filepath, err); 68 | cb(err, undefined); 69 | }, 70 | ); 71 | }; 72 | return fileinfo; 73 | } 74 | 75 | constructor(private http: HttpClient) {} 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/app/features/gmcp/gmcp-config.ts: -------------------------------------------------------------------------------- 1 | import { GmcpMenu } from './gmcp-menu'; 2 | 3 | export class GmcpConfig { 4 | module_name = ''; // modulename and version 5 | version = ''; 6 | mud_id = ''; // nud connection id. 7 | mud_family = ''; // mud family defines detail behavior. 8 | initial_menu: GmcpMenu = { 9 | action: '', 10 | active: false, 11 | name: '', 12 | mud_id: '', 13 | cfg: this, 14 | index: 0, 15 | }; 16 | callback: any; // callback for actions. 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/features/gmcp/gmcp-menu.ts: -------------------------------------------------------------------------------- 1 | import { GmcpConfig } from './gmcp-config'; 2 | 3 | export interface GmcpMenu { 4 | name: string; 5 | active: boolean; 6 | action: string; 7 | index: number; 8 | mud_id: string; 9 | cfg: GmcpConfig; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/features/gmcp/gmcp.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | declarations: [], 7 | exports: [], 8 | }) 9 | export class GmcpModule {} 10 | -------------------------------------------------------------------------------- /frontend/src/app/features/gmcp/index.ts: -------------------------------------------------------------------------------- 1 | export { GmcpModule } from './gmcp.module'; 2 | export { GmcpService } from './gmcp.service'; -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/box.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@angular/core'; 2 | 3 | export class Box { 4 | left?: number; 5 | top?: number; 6 | right?: number; 7 | bottom?: number; 8 | width?: number; 9 | height?: number; 10 | outerBox?: Box | null = null; 11 | minBox?: Box | null = null; 12 | outEvent: EventEmitter = null; 13 | init_absolute(left: number, top: number, right: number, bottom: number) { 14 | this.left = left; 15 | this.top = top; 16 | this.right = right; 17 | this.bottom = bottom; 18 | this.width = this.right - this.left; 19 | this.height = this.bottom - this.top; 20 | } 21 | 22 | init_relative(left: number, top: number, width: number, height: number) { 23 | this.left = left; 24 | this.top = top; 25 | this.width = width; 26 | this.height = height; 27 | this.right = this.left + width; 28 | this.bottom = this.top + height; 29 | } 30 | resize_to_mouse(mouse_x: number, mouse_y: number) { 31 | // (this.mouse.x < this.containerPos.right && this.mouse.y < this.containerPos.bottom 32 | let width = this.width; 33 | let height = this.height; 34 | let count_width_changes = 0; 35 | let count_height_changes = 0; 36 | if (this.outerBox != null) { 37 | if (mouse_x >= this.outerBox.right || mouse_y >= this.outerBox.bottom) { 38 | return; 39 | } 40 | width = Number(mouse_x > this.outerBox.left) 41 | ? mouse_x - this.outerBox.left 42 | : 0; 43 | height = Number(mouse_y > this.outerBox.top) 44 | ? mouse_y - this.outerBox.top 45 | : 0; 46 | if (this.left + width > this.outerBox.right) { 47 | width = this.outerBox.right - this.left; 48 | count_width_changes++; 49 | } 50 | if (this.top + height > this.outerBox.bottom) { 51 | height = this.outerBox.bottom - this.top; 52 | count_height_changes++; 53 | } 54 | } else { 55 | return; 56 | } 57 | if (this.minBox != null) { 58 | if (width < this.minBox.width) { 59 | width = this.minBox.width; 60 | count_width_changes++; 61 | } 62 | if (height < this.minBox.height) { 63 | height = this.minBox.height; 64 | count_height_changes++; 65 | } 66 | } 67 | let changeflag = 0; 68 | if (count_width_changes < 2 && width != this.width) { 69 | this.width = width; 70 | this.right = this.left + this.width; 71 | changeflag++; 72 | } 73 | if (count_height_changes < 2 && height != this.height) { 74 | this.height = height; 75 | this.bottom = this.top + this.height; 76 | changeflag++; 77 | } 78 | if (changeflag > 0 && this.outEvent != null) { 79 | this.outEvent.next(this); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/char-stat/char-stat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Charaktername:
3 |
{{charData.nameAtMud}}
4 |
Status:
5 |
{{charData.cStatus}}
6 |
Vitals:
7 |
{{charData.cVitals}}
8 |
9 | Daten werden geladen! 10 |
11 |
12 |
{{stat.name}}
13 |
{{stat.value}}
14 |
15 |
-------------------------------------------------------------------------------- /frontend/src/app/features/modeless/char-stat/char-stat.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/modeless/char-stat/char-stat.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/char-stat/char-stat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { CharacterData, WindowConfig } from '@mudlet3/frontend/shared'; 3 | 4 | @Component({ 5 | selector: 'app-char-stat', 6 | templateUrl: './char-stat.component.html', 7 | styleUrls: ['./char-stat.component.scss'], 8 | }) 9 | export class CharStatComponent implements OnInit { 10 | private _config?: WindowConfig; 11 | 12 | @Input() set config(cfg: WindowConfig) { 13 | this._config = cfg; 14 | console.log('CharStat-config:', cfg); 15 | this.charData = cfg.data; 16 | } 17 | get config(): WindowConfig | undefined { 18 | return this._config; 19 | } 20 | 21 | public charData?: CharacterData; 22 | 23 | ngOnInit(): void { 24 | console.debug('inComingEvents-CharStat-1'); 25 | this.config?.inComingEvents.subscribe( 26 | (event: string) => { 27 | console.log('inComingEvents-CharStat-2', event, this.charData); 28 | }, 29 | (error) => { 30 | console.error('incomingEvents-CharStat-3', error); 31 | }, 32 | () => { 33 | if (this.config !== undefined) { 34 | this.config.visible = false; 35 | } 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/dirlist/dirlist.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ path }} 4 |
5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 |
9 | {{ entry.name }} 15 | {{ entry.size }}{{ entry.filedate }}{{ entry.filetime }}
22 | {{ entry.name }}/ 28 | {{ entry.filedate }}{{ entry.filetime }}
35 |
36 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/dirlist/dirlist.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/modeless/dirlist/dirlist.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/dirlist/dirlist.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { 3 | FileEntries, 4 | MudSignals, 5 | WindowConfig, 6 | } from '@mudlet3/frontend/shared'; 7 | 8 | @Component({ 9 | selector: 'app-dirlist', 10 | templateUrl: './dirlist.component.html', 11 | styleUrls: ['./dirlist.component.scss'], 12 | }) 13 | export class DirlistComponent implements OnInit { 14 | private musi?: MudSignals; 15 | private _config?: WindowConfig; 16 | 17 | @Input() 18 | set config(cfg: WindowConfig) { 19 | this._config = cfg; 20 | // console.log("config:",cfg); 21 | this.updateDirList(); 22 | } 23 | 24 | get config(): WindowConfig | undefined { 25 | return this._config; 26 | } 27 | 28 | public path? = ''; 29 | 30 | public entries?: FileEntries[] = []; 31 | 32 | updateDirList() { 33 | if (this._config === undefined) { 34 | throw new Error('config is undefined and should not be!'); 35 | } 36 | 37 | this.musi = this._config.data; 38 | 39 | if (typeof this.musi === 'undefined') { 40 | this.path = ''; 41 | this.entries = []; 42 | return; 43 | } 44 | 45 | this.path = this.musi.filepath; 46 | this.entries = this.musi.entries; 47 | 48 | console.debug('DirlistComponent-updateDirList', this.path); 49 | } 50 | 51 | fileOpen(file: string) { 52 | this.config?.outGoingEvents.next('FileOpen:' + this.path + ':' + file); 53 | } 54 | 55 | changeDir(dir: string) { 56 | this.config?.outGoingEvents.next('ChangeDir:' + this.path + ':' + dir); 57 | } 58 | 59 | ngOnInit(): void { 60 | console.debug('inComingEvents-DirList'); 61 | this.config?.inComingEvents.subscribe( 62 | (event: string) => { 63 | this.updateDirList(); 64 | console.log('inComingEvents-DirList', event); 65 | }, 66 | (error) => { 67 | console.error('incomingEvents-DirList', error); 68 | }, 69 | () => { 70 | if (this.config !== undefined) { 71 | this.config.visible = false; 72 | } 73 | }, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/editor/editor.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 9 | 21 | 22 |
23 |
24 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/editor/editor.component.scss: -------------------------------------------------------------------------------- 1 | .app-ace-editor { 2 | border: 2px solid #f8f9fa; 3 | box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 4 | resize:both; 5 | overflow:auto; 6 | } -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/flexible-area/flexible-area.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /frontend/src/app/features/modeless/flexible-area/flexible-area.component.scss: -------------------------------------------------------------------------------- 1 | .box-container { 2 | position: absolute; 3 | width: 600px; 4 | height: 450px; 5 | outline: 1px solid black; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate3d(-50%, -50%, 0); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/flexible-area/flexible-area.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-flexible-area', 5 | templateUrl: './flexible-area.component.html', 6 | styleUrls: ['./flexible-area.component.scss'], 7 | }) 8 | export class FlexibleAreaComponent {} 9 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/index.ts: -------------------------------------------------------------------------------- 1 | export { KeypadConfigComponent } from './keypad-config/keypad-config.component'; -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keyone/keyone.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keyone/keyone.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/modeless/keyone/keyone.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keyone/keyone.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | ViewChild, 8 | } from '@angular/core'; 9 | 10 | @Component({ 11 | selector: 'app-keyone', 12 | templateUrl: './keyone.component.html', 13 | styleUrls: ['./keyone.component.scss'], 14 | }) 15 | export class KeyoneComponent { 16 | @Input({ required: true }) 17 | public prefix!: string; 18 | 19 | @Input({ required: true }) 20 | public key!: string; 21 | 22 | @Input() 23 | set value(val: string) { 24 | this.keyinp = val; 25 | } 26 | get value(): string { 27 | return this.keyinp; 28 | } 29 | 30 | @Output() 31 | public keyAction = new EventEmitter(); 32 | 33 | @ViewChild('keyoneInput', { static: false }) 34 | public keyoneInput?: ElementRef; 35 | 36 | public keyinp = ''; 37 | 38 | submit() { 39 | this.keyAction.emit(this.prefix + ':' + this.key + ':' + this.keyinp); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad-config/keypad-config.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad-config/keypad-config.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/modeless/keypad-config/keypad-config.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad-config/keypad-config.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { KeypadData } from '@mudlet3/frontend/shared'; 3 | import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; 4 | 5 | @Component({ 6 | selector: 'app-keypad-config', 7 | templateUrl: './keypad-config.component.html', 8 | styleUrls: ['./keypad-config.component.scss'], 9 | }) 10 | export class KeypadConfigComponent implements OnInit { 11 | public keypad: KeypadData = new KeypadData(); 12 | 13 | cb?: Function; 14 | 15 | cbThis: any; // paththrough 16 | 17 | public keyAction(event: string) { 18 | const newev = { 19 | item: { 20 | id: 'MUD:NUMPAD:RETURN', 21 | cbThis: this.cbThis, 22 | keypad: this.keypad, 23 | event: event, 24 | }, 25 | }; 26 | 27 | this.cb?.(newev); 28 | } 29 | 30 | constructor( 31 | public ref: DynamicDialogRef, 32 | public config: DynamicDialogConfig, 33 | ) {} 34 | 35 | ngOnInit(): void { 36 | this.keypad = this.config.data.keypad; 37 | this.cb = this.config.data['cb']; 38 | this.cbThis = this.config.data['cbThis']; 39 | console.log('KeypadConfigComponent-1', this.keypad); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad/keypad.component.html: -------------------------------------------------------------------------------- 1 |
2 |
-
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Daten werden geladen! -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad/keypad.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/modeless/keypad/keypad.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/keypad/keypad.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { OneKeypadData } from '@mudlet3/frontend/shared'; 3 | 4 | @Component({ 5 | selector: 'app-keypad', 6 | templateUrl: './keypad.component.html', 7 | styleUrls: ['./keypad.component.scss'], 8 | }) 9 | export class KeypadComponent { 10 | @Input() set keypad(one: OneKeypadData) { 11 | if (typeof one !== 'undefined') this._keypad = one; 12 | } 13 | get config(): OneKeypadData { 14 | return this._keypad; 15 | } 16 | _keypad: OneKeypadData = new OneKeypadData(''); 17 | 18 | @Output() keyAction = new EventEmitter(); 19 | 20 | valAction(event: string) { 21 | const evs = event.split(':'); 22 | switch (evs[1]) { 23 | case '/': 24 | evs[1] = 'NumpadDivide'; 25 | break; 26 | case '*': 27 | evs[1] = 'NumpadMultiply'; 28 | break; 29 | case '-': 30 | evs[1] = 'NumpadSubtract'; 31 | break; 32 | case '+': 33 | evs[1] = 'NumpadAdd'; 34 | break; 35 | case ',': 36 | evs[1] = 'NumpadDecimal'; 37 | break; 38 | case '0': 39 | case '1': 40 | case '2': 41 | case '3': 42 | case '4': 43 | case '5': 44 | case '6': 45 | case '7': 46 | case '8': 47 | case '9': 48 | evs[1] = 'Numpad' + evs[1]; 49 | break; 50 | default: 51 | console.log('keypad-unknown event:', event); 52 | return; 53 | } 54 | this.keyAction.emit(evs.join(':')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/modeless.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { PrimeModule } from '@mudlet3/frontend/shared'; 5 | 6 | import { CharStatComponent } from './char-stat/char-stat.component'; 7 | import { DirlistComponent } from './dirlist/dirlist.component'; 8 | import { EditorComponent } from './editor/editor.component'; 9 | import { FlexibleAreaComponent } from './flexible-area/flexible-area.component'; 10 | import { KeyoneComponent } from './keyone/keyone.component'; 11 | import { KeypadComponent } from './keypad/keypad.component'; 12 | import { KeypadConfigComponent } from './keypad-config/keypad-config.component'; 13 | import { ResizableDraggableComponent } from './resizable-draggable/resizable-draggable.component'; 14 | import { WindowComponent } from './window/window.component'; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | ResizableDraggableComponent, 19 | FlexibleAreaComponent, 20 | WindowComponent, 21 | DirlistComponent, 22 | EditorComponent, 23 | KeypadComponent, 24 | KeypadConfigComponent, 25 | KeyoneComponent, 26 | CharStatComponent, 27 | ], 28 | imports: [CommonModule, FormsModule, PrimeModule], 29 | providers: [], 30 | exports: [ 31 | ResizableDraggableComponent, 32 | FlexibleAreaComponent, 33 | WindowComponent, 34 | ], 35 | }) 36 | export class ModelessModule {} 37 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/resizable-draggable/resizable-draggable.component.html: -------------------------------------------------------------------------------- 1 |
9 |
10 | {{width | number:'1.0-0'}}px 11 | {{height | number:'1.0-0'}}px 12 | ({{left}}, {{top}}) 13 |
-------------------------------------------------------------------------------- /frontend/src/app/features/modeless/resizable-draggable/resizable-draggable.component.scss: -------------------------------------------------------------------------------- 1 | .resizable-draggable { 2 | outline: 1px dashed green; 3 | } 4 | .resizable-draggable.active { 5 | outline-style: solid; 6 | background-color: #80ff80 0d; 7 | } 8 | .resizable-draggable:hover { 9 | cursor: all-scroll; 10 | } 11 | .resizable-draggable span:first-of-type { 12 | position: absolute; 13 | left: 50%; 14 | transform: translate3d(-50%, -100%, 0); 15 | } 16 | .resizable-draggable span:nth-of-type(2) { 17 | position: absolute; 18 | top: 50%; 19 | transform: translate3d(-100%, -50%, 0); 20 | } 21 | .resizable-draggable span:nth-of-type(3) { 22 | position: absolute; 23 | transform: translate3d(-100%, -100%, 0); 24 | } 25 | .resize-action { 26 | position: absolute; 27 | left: 100%; 28 | top: 100%; 29 | transform: translate3d(-50%, -50%, 0) rotateZ(45deg); 30 | border-style: solid; 31 | border-width: 8px; 32 | border-color: transparent transparent transparent #008000; 33 | } 34 | .resize-action:hover, .resize-action:active { 35 | cursor: nwse-resize; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/window/window.component.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 23 | 27 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/window/window.component.scss: -------------------------------------------------------------------------------- 1 | .p-dialog,.p-dialog-content { 2 | overflow: hidden; 3 | resize:both; 4 | } -------------------------------------------------------------------------------- /frontend/src/app/features/modeless/window/window.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | ViewChild, 7 | } from '@angular/core'; 8 | import { WindowConfig } from '@mudlet3/frontend/shared'; 9 | 10 | @Component({ 11 | selector: 'app-window', 12 | templateUrl: './window.component.html', 13 | styleUrls: ['./window.component.scss'], 14 | }) 15 | export class WindowComponent { 16 | @Input({ required: true }) 17 | public config!: WindowConfig; 18 | 19 | @Output() 20 | public menuAction = new EventEmitter(); 21 | 22 | @ViewChild('dialog', { static: false }) 23 | public dialog?: any; 24 | 25 | doWindowAction(event: any, actionType: string) { 26 | //console.log(actionType,event); 27 | switch (actionType) { 28 | case 'resize_end': 29 | this.config.inComingEvents.next( 30 | 'resize:' + event.pageX + ':' + event.pageY, 31 | ); 32 | return; 33 | case 'drag_end': 34 | this.config.outGoingEvents.next('do_focus:' + this.config.windowid); 35 | return; 36 | case 'hide': 37 | this.config.outGoingEvents.next('do_hide:' + this.config.windowid); 38 | return; 39 | case 'show': 40 | case 'resize_init': 41 | case 'maximize': 42 | break; 43 | default: 44 | return; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/app/features/mudconfig/index.ts: -------------------------------------------------------------------------------- 1 | export { MudConfig } from './mud-config'; 2 | export { MudconfigModule } from './mudconfig.module'; -------------------------------------------------------------------------------- /frontend/src/app/features/mudconfig/mud-config.ts: -------------------------------------------------------------------------------- 1 | export interface MudConfig { 2 | mudname: string; 3 | height: number; 4 | width: number; 5 | 6 | user?: string; 7 | token?: string; 8 | password?: string; 9 | 10 | browser?: any; 11 | client?: string; 12 | version?: string; 13 | 14 | 'gmcp-mudname'?: string; 15 | 'gmcp-charname'?: string; 16 | 'gmcp-fullname'?: string; 17 | 'gmcp-gender'?: string; 18 | 'gmcp-wizard'?: string; 19 | 'guild-varname'?: string; 20 | 'race-varname'?: string; 21 | 'rank-varname'?: string; 22 | guild?: string; 23 | race?: string; 24 | rank?: string; 25 | 'sound-url'?: string; 26 | 'dir-current'?: string; 27 | 'dir-entries'?: string; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/features/mudconfig/mudconfig.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | declarations: [], 7 | }) 8 | export class MudconfigModule {} 9 | -------------------------------------------------------------------------------- /frontend/src/app/features/settings/color-settings/color-settings.component.html: -------------------------------------------------------------------------------- 1 |
8 |
15 |
22 |
29 | 34 |
40 | Ergebnis
46 | -------------------------------------------------------------------------------- /frontend/src/app/features/settings/color-settings/color-settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/settings/color-settings/color-settings.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/settings/color-settings/color-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ColorSettings } from '@mudlet3/frontend/shared'; 3 | import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; 4 | 5 | @Component({ 6 | selector: 'app-color-settings', 7 | templateUrl: './color-settings.component.html', 8 | styleUrls: ['./color-settings.component.scss'], 9 | }) 10 | export class ColorSettingsComponent implements OnInit { 11 | cs: ColorSettings = new ColorSettings(); 12 | cb: any; 13 | v: any; // pass through to MenuAction cb! 14 | cbThis: any; // paththrough 15 | 16 | constructor( 17 | public ref: DynamicDialogRef, 18 | public config: DynamicDialogConfig, 19 | ) {} 20 | 21 | ngOnInit(): void { 22 | this.cs = this.config.data['cs']; 23 | this.cb = this.config.data['cb']; 24 | this.v = this.config.data['v']; 25 | this.cbThis = this.config.data['cbThis']; 26 | } 27 | onClick(event: any) { 28 | const newev = { 29 | item: { 30 | id: 'MUD_VIEW:COLOR:RETURN', 31 | cs: this.cs, 32 | v: this.v, 33 | cbThis: this.cbThis, 34 | }, 35 | }; 36 | this.cb(newev); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/features/settings/editor-search/editor-search.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/features/settings/editor-search/editor-search.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/app/features/settings/editor-search/editor-search.component.scss -------------------------------------------------------------------------------- /frontend/src/app/features/settings/editor-search/editor-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { WindowConfig } from '@mudlet3/frontend/shared'; 3 | import * as ace from 'ace-builds'; 4 | 5 | @Component({ 6 | selector: 'app-editor-search', 7 | templateUrl: './editor-search.component.html', 8 | styleUrls: ['./editor-search.component.scss'], 9 | }) 10 | export class EditorSearchComponent implements OnInit { 11 | @Input() set config(cfg: WindowConfig) { 12 | this._config = cfg; 13 | this.aceEditor = this.config?.data['aceEditor']; 14 | console.log('config:', cfg); 15 | } 16 | get config(): WindowConfig | undefined { 17 | return this._config; 18 | } 19 | private _config?: WindowConfig; 20 | 21 | private aceEditor?: ace.Ace.Editor; 22 | 23 | seachText = ''; 24 | type = 'text'; 25 | 26 | ngOnInit(): void { 27 | // this.aceEditor = this.config.data['aceEDitor']; 28 | return; 29 | } 30 | onSearch() { 31 | return; 32 | // var range = this.aceEditor.find(this.seachText,{ 33 | // wrap: true, 34 | // caseSensitive: true, 35 | // }) 36 | } 37 | onReplace() { 38 | return; 39 | } 40 | doSearch() { 41 | return; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/features/settings/index.ts: -------------------------------------------------------------------------------- 1 | export { ColorSettingsComponent } from './color-settings/color-settings.component'; 2 | export { SettingsModule } from './settings.module'; -------------------------------------------------------------------------------- /frontend/src/app/features/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { PrimeModule } from '@mudlet3/frontend/shared'; 6 | 7 | import { ColorSettingsComponent } from './color-settings/color-settings.component'; 8 | import { EditorSearchComponent } from './editor-search/editor-search.component'; 9 | 10 | @NgModule({ 11 | declarations: [ColorSettingsComponent, EditorSearchComponent], 12 | imports: [CommonModule, BrowserModule, FormsModule, PrimeModule], 13 | exports: [ColorSettingsComponent, EditorSearchComponent], 14 | }) 15 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 16 | export class SettingsModule {} 17 | -------------------------------------------------------------------------------- /frontend/src/app/features/sockets/index.ts: -------------------------------------------------------------------------------- 1 | export { SocketsService } from './sockets.service'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/features/sockets/types/client-to-server-events.ts: -------------------------------------------------------------------------------- 1 | export interface ClientToServerEvents { 2 | mudConnect: () => void; 3 | mudDisconnect: () => void; 4 | mudInput: (data: string) => void; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/app/features/sockets/types/server-to-client-events.ts: -------------------------------------------------------------------------------- 1 | export interface ServerToClientEvents { 2 | mudOutput: (data: string) => void; 3 | mudDisconnected: () => void; 4 | mudConnected: () => void; 5 | setEchoMode: (showEchos: boolean) => void; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app/features/widgets/index.ts: -------------------------------------------------------------------------------- 1 | export { WidgetsModule } from './widgets.module'; -------------------------------------------------------------------------------- /frontend/src/app/features/widgets/inventory/inventory.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
  • 4 | {{icat}} 5 |
      6 |
    • {{invitem}}
    • 7 |
    8 |
  • 9 |
10 |
11 | -------------------------------------------------------------------------------- /frontend/src/app/features/widgets/inventory/inventory.component.scss: -------------------------------------------------------------------------------- 1 | .invcategoryname { 2 | font-weight: bold; 3 | } 4 | 5 | ul.inventory { 6 | padding: 0; 7 | } 8 | 9 | ul.inventory li { 10 | list-style-type: none; 11 | } 12 | 13 | .invcategory { 14 | padding-left: 2em; 15 | } 16 | 17 | 18 | .p-scrollpanel { 19 | &.custombar1 { 20 | .p-scrollpanel-wrapper { 21 | border-right: 9px solid var(--layer-1); 22 | } 23 | 24 | .p-scrollpanel-bar { 25 | background-color: var(--primary-color) !important; 26 | opacity: 1 !important; 27 | transition: 0 !important; 28 | 29 | &:hover { 30 | background-color: #007ad9 !important; 31 | opacity: 1 !important; 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /frontend/src/app/features/widgets/inventory/inventory.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { InventoryList } from '@mudlet3/frontend/shared'; 3 | 4 | @Component({ 5 | selector: 'app-inventory', 6 | templateUrl: './inventory.component.html', 7 | styleUrls: ['./inventory.component.scss'], 8 | }) 9 | export class InventoryComponent { 10 | @Input({ required: true }) inv!: InventoryList; 11 | @Input({ required: true }) vheight!: number; 12 | 13 | public mystyle(): string { 14 | if (this.vheight === 0) { 15 | this.vheight = 300; 16 | } 17 | return `{width: '100%', height: ${this.vheight.toString()}px'}`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/features/widgets/widgets.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { PrimeModule } from '@mudlet3/frontend/shared'; 7 | 8 | import { InventoryComponent } from './inventory/inventory.component'; 9 | 10 | @NgModule({ 11 | declarations: [InventoryComponent], 12 | imports: [ 13 | CommonModule, 14 | FormsModule, 15 | BrowserModule, 16 | BrowserAnimationsModule, 17 | PrimeModule, 18 | ], 19 | exports: [InventoryComponent], 20 | }) 21 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 22 | export class WidgetsModule {} 23 | -------------------------------------------------------------------------------- /frontend/src/app/shared/WINDOW_PROVIDERS.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider, InjectionToken } from '@angular/core'; 2 | 3 | export const WINDOW = new InjectionToken('window'); 4 | 5 | const windowProvider: FactoryProvider = { 6 | provide: WINDOW, 7 | useFactory: () => window, 8 | }; 9 | 10 | export const WINDOW_PROVIDERS = [windowProvider]; 11 | -------------------------------------------------------------------------------- /frontend/src/app/shared/char-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { CharacterData } from './char-data'; 2 | 3 | describe('CharacterData', () => { 4 | let characterData: CharacterData; 5 | 6 | beforeEach(() => { 7 | characterData = new CharacterData('TestCharacter'); 8 | }); 9 | 10 | test('should initialize with the correct name', () => { 11 | expect(characterData.nameAtMud).toBe('TestCharacter'); 12 | }); 13 | 14 | test('should set status correctly', () => { 15 | characterData.setStatus('Healthy'); 16 | expect(characterData.cStatus).toBe('Healthy'); 17 | }); 18 | 19 | test('should set vitals correctly', () => { 20 | characterData.setVitals('Alive|Dead'); 21 | expect(characterData.cVitals).toBe('Alive'); 22 | }); 23 | 24 | test('should set stats correctly', () => { 25 | const statsInput = 'str=59.8|int=130|con=34.2|dex=59.7'; 26 | characterData.setStats(statsInput); 27 | 28 | expect(characterData.cStats.length).toBe(4); 29 | 30 | expect(characterData.cStats[0]).toEqual({ 31 | key: 'str', 32 | name: 'Stärke', 33 | value: '59.8', 34 | }); 35 | 36 | expect(characterData.cStats[1]).toEqual({ 37 | key: 'int', 38 | name: 'Intelligenz', 39 | value: '130', 40 | }); 41 | 42 | expect(characterData.cStats[2]).toEqual({ 43 | key: 'con', 44 | name: 'Ausdauer', 45 | value: '34.2', 46 | }); 47 | 48 | expect(characterData.cStats[3]).toEqual({ 49 | key: 'dex', 50 | name: 'Geschicklichkeit', 51 | value: '59.7', 52 | }); 53 | }); 54 | 55 | test('should handle unknown stats gracefully', () => { 56 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); 57 | const statsInput = 'str=59.8|int=130|con=34.2|unknown=42|dex=59.7'; 58 | characterData.setStats(statsInput); 59 | 60 | expect(characterData.cStats.length).toBe(4); 61 | 62 | expect(characterData.cStats[0]).toEqual({ 63 | key: 'str', 64 | name: 'Stärke', 65 | value: '59.8', 66 | }); 67 | 68 | expect(characterData.cStats[1]).toEqual({ 69 | key: 'int', 70 | name: 'Intelligenz', 71 | value: '130', 72 | }); 73 | 74 | expect(characterData.cStats[2]).toEqual({ 75 | key: 'con', 76 | name: 'Ausdauer', 77 | value: '34.2', 78 | }); 79 | 80 | expect(characterData.cStats[3]).toEqual({ 81 | key: 'dex', 82 | name: 'Geschicklichkeit', 83 | value: '59.7', 84 | }); 85 | 86 | expect(consoleSpy).toHaveBeenCalledWith('Unknown Stat', ['unknown', '42']); 87 | consoleSpy.mockRestore(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /frontend/src/app/shared/char-data.ts: -------------------------------------------------------------------------------- 1 | export class CharacterData { 2 | public nameAtMud = ''; 3 | public cStatus = ''; 4 | public cVitals = ''; 5 | public cStats: CharacterStat[] = []; 6 | 7 | constructor(name: string) { 8 | this.nameAtMud = name; 9 | } 10 | 11 | public setStatus(inp: string) { 12 | this.cStatus = inp; 13 | } 14 | public setVitals(inp: string) { 15 | const csplit = inp.split('|'); 16 | this.cVitals = csplit[0]; 17 | } 18 | public setStats(inp: string) { 19 | const csplit = inp.split('|'); 20 | const csArr: string[] = ['str', 'int', 'con', 'dex']; 21 | let i = 0; 22 | const tmpOb: any = {}; 23 | for (i = 0; i < csplit.length; i++) { 24 | const params = csplit[i].split('='); 25 | const statOb = new CharacterStat(); 26 | switch (params[0]) { 27 | case 'str': 28 | statOb.name = 'Stärke'; 29 | break; 30 | case 'int': 31 | statOb.name = 'Intelligenz'; 32 | break; 33 | case 'con': 34 | statOb.name = 'Ausdauer'; 35 | break; 36 | case 'dex': 37 | statOb.name = 'Geschicklichkeit'; 38 | break; 39 | default: 40 | console.error('Unknown Stat', params); 41 | continue; 42 | } 43 | statOb.key = params[0]; 44 | statOb.value = params[1]; 45 | tmpOb[params[0]] = statOb; 46 | } 47 | this.cStats = []; 48 | for (i = 0; i < csArr.length; i++) { 49 | this.cStats.push(tmpOb[csArr[i]]); 50 | } 51 | console.log('setStats', this.cStats, csArr, tmpOb); 52 | } 53 | 54 | // con=34,2|dex=59,7|int=130|str=59,8 55 | // Stärke: 59,8 Intelligenz: 130 Ausdauer: 34,2 Geschicklichkeit: 59,7 56 | } 57 | 58 | export class CharacterStat { 59 | public key = ''; 60 | public name = ''; 61 | public value = ''; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/app/shared/color-settings.ts: -------------------------------------------------------------------------------- 1 | export class ColorSettings { 2 | invert = false; 3 | blackOnWhite = false; 4 | colorOff = false; 5 | localEchoColor: string = '#a8ff00'; 6 | localEchoBackground: string = '#000000'; 7 | localEchoActive = true; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/shared/file-info.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-empty-function: "warn" */ 2 | export class FileInfo { 3 | lasturl = ''; 4 | file = ''; 5 | path = ''; 6 | filename = ''; 7 | filetype = ''; 8 | edditortype? = ''; 9 | 10 | newfile = true; 11 | writeacl = false; 12 | temporary = false; 13 | saveActive = false; 14 | closable = false; 15 | filesize = -1; 16 | title = ''; 17 | 18 | content? = ''; 19 | oldContent? = ''; 20 | 21 | alreadyLoaded? = false; 22 | windowsId?: string = undefined; 23 | 24 | save01_start? = function (filepath: string) {}; 25 | save02_url? = function (url: string) {}; 26 | save03_saved? = function (filepath: string) {}; 27 | save04_closing? = function (windowsid: string) {}; 28 | save05_error? = function (windowsid: string, error: string) {}; 29 | save06_success? = function (windowsid: string) {}; 30 | relateWindow? = function (windowsid: string) {}; 31 | load? = function (cb: Function) {}; 32 | cancel01_start? = function (filepath: string, cb: Function) {}; 33 | cancel02_end? = function (filepath: string) {}; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { CharacterData } from './char-data'; 2 | export { ColorSettings } from './color-settings'; 3 | export { FileInfo } from './file-info'; 4 | export { InventoryEntry, InventoryList } from './inventory-list'; 5 | export { KeypadData, OneKeypadData } from './keypad-data'; 6 | export { MudListItem } from './mud-list-item'; 7 | export { FileEntries, MudSignals } from './mud-signals'; 8 | export * from './prime.module'; 9 | export { ServerConfigService } from './server-config.service'; 10 | export { WithRequired } from './types/with-required'; 11 | export { wordWrap } from './utils/word-wrap'; 12 | export { WindowService } from './window.service'; 13 | export { WINDOW, WINDOW_PROVIDERS } from './WINDOW_PROVIDERS'; 14 | export { WindowConfig } from './window-config'; 15 | export { SecureString, isSecureString } from './types/secure-string'; 16 | -------------------------------------------------------------------------------- /frontend/src/app/shared/inventory-list.ts: -------------------------------------------------------------------------------- 1 | export interface InventoryEntry { 2 | name: string; 3 | category: string; 4 | } 5 | 6 | export class InventoryList { 7 | private namedList: Record = {}; 8 | 9 | public getItems(cat: string): string[] { 10 | return this.namedList[cat]; 11 | } 12 | 13 | public getCategories(): string[] { 14 | return Object.keys(this.namedList); 15 | } 16 | 17 | public addItem(ientry: InventoryEntry, addTop = true) { 18 | if (Object.prototype.hasOwnProperty.call(this.namedList, ientry.category)) { 19 | if (addTop) { 20 | this.namedList[ientry.category].unshift(ientry.name); 21 | } else { 22 | this.namedList[ientry.category].push(ientry.name); 23 | } 24 | } else { 25 | this.namedList[ientry.category] = [ientry.name]; 26 | } 27 | } 28 | 29 | public removeItem(ientry: InventoryEntry) { 30 | if (Object.prototype.hasOwnProperty.call(this.namedList, ientry.category)) { 31 | const ix = this.namedList[ientry.category].indexOf(ientry.name); 32 | if (ix >= 0) { 33 | this.namedList[ientry.category].splice(ix, 1); 34 | if (this.namedList[ientry.category].length == 0) { 35 | delete this.namedList[ientry.category]; 36 | } 37 | } 38 | } 39 | } 40 | 41 | public initList(ilist: InventoryEntry[]) { 42 | this.namedList = {}; 43 | 44 | ilist.forEach((currentValue, index, arr) => { 45 | this.addItem(currentValue, false); 46 | }, this); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/app/shared/keypad-data.ts: -------------------------------------------------------------------------------- 1 | export class OneKeypadData { 2 | public prefix = ''; 3 | public keys: any = {}; 4 | public addKey(key: string, value: string) { 5 | this.keys[key] = value; 6 | } 7 | public getKey(key: string): string { 8 | return this.keys[key] || ''; 9 | } 10 | constructor(p: string) { 11 | this.prefix = p; 12 | } 13 | } 14 | 15 | export class KeypadData { 16 | public levels: any = {}; 17 | public addKey(prefix: string, key: string, value: string) { 18 | if (typeof this.levels[prefix] === 'undefined') { 19 | const level = new OneKeypadData(prefix); 20 | level.addKey(key, value); 21 | this.levels[prefix] = level; 22 | } else { 23 | this.levels[prefix].addKey(key, value); 24 | } 25 | } 26 | public getLevel(prefix: string): OneKeypadData { 27 | if (typeof this.levels[prefix] === 'undefined') { 28 | const level = new OneKeypadData(prefix); 29 | this.levels[prefix] = level; 30 | } 31 | return this.levels[prefix]; 32 | } 33 | public getCompoundKey(modifiers: string): string { 34 | const msplit = modifiers.split('|'); 35 | const lvl = this.getLevel(msplit[0]); 36 | const val = lvl.getKey(msplit[1]); 37 | if (val == '') { 38 | console.log('CompondKey Empty:', modifiers); 39 | } 40 | return val; 41 | } 42 | public setLevel(numpad: OneKeypadData) { 43 | this.levels[numpad.prefix] = numpad; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/app/shared/mud-list-item.ts: -------------------------------------------------------------------------------- 1 | // Todo: Schauen, ob wir das entfernt bekommen. Der Client connected sich nicht zu Muds. Er connected sich ausschließlich zum Server. 2 | // Todo: Löschen 3 | export interface MudListItem { 4 | key: string; 5 | name: string; 6 | host: string; 7 | port: number; 8 | ssl: boolean; 9 | rejectUnauthorized: boolean; 10 | description: string; 11 | playerlevel: string; 12 | mudfamily: string; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/shared/mud-signals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileInfo, 3 | InventoryEntry, 4 | OneKeypadData, 5 | } from '@mudlet3/frontend/shared'; 6 | 7 | export class FileEntries { 8 | name = ''; 9 | size = -2; 10 | filedate = ''; 11 | filetime = ''; 12 | isdir = 0; 13 | } 14 | 15 | export interface MudSignals { 16 | signal: string; 17 | id: string; 18 | wizard?: number; 19 | playSoundFile?: string; 20 | filepath?: string; 21 | fileinfo?: FileInfo; 22 | entries?: FileEntries[]; 23 | numpadLevel?: OneKeypadData; 24 | invEntry?: InventoryEntry; 25 | invEntries?: InventoryEntry[]; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/shared/prime.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ConfirmationService, MessageService } from 'primeng/api'; 4 | import { AutoFocusModule } from 'primeng/autofocus'; 5 | import { ButtonModule } from 'primeng/button'; 6 | import { CheckboxModule } from 'primeng/checkbox'; 7 | import { ColorPickerModule } from 'primeng/colorpicker'; 8 | import { ConfirmPopupModule } from 'primeng/confirmpopup'; 9 | import { DialogModule } from 'primeng/dialog'; 10 | import { DividerModule } from 'primeng/divider'; 11 | import { DropdownModule } from 'primeng/dropdown'; 12 | import { DialogService, DynamicDialogModule } from 'primeng/dynamicdialog'; 13 | import { FocusTrapModule } from 'primeng/focustrap'; 14 | import { InputTextModule } from 'primeng/inputtext'; 15 | import { InputTextareaModule } from 'primeng/inputtextarea'; 16 | import { MenuModule } from 'primeng/menu'; 17 | import { MenubarModule } from 'primeng/menubar'; 18 | import { ScrollPanelModule } from 'primeng/scrollpanel'; 19 | import { SlideMenuModule } from 'primeng/slidemenu'; 20 | import { TableModule } from 'primeng/table'; 21 | import { TabViewModule } from 'primeng/tabview'; 22 | import { ToastModule } from 'primeng/toast'; 23 | import { ToolbarModule } from 'primeng/toolbar'; 24 | 25 | // Todo[myst]: Remove this whole module and import on the fly 26 | @NgModule({ 27 | declarations: [], 28 | imports: [ 29 | CommonModule, 30 | FocusTrapModule, 31 | AutoFocusModule, 32 | InputTextModule, 33 | DividerModule, 34 | CheckboxModule, 35 | ScrollPanelModule, 36 | ColorPickerModule, 37 | SlideMenuModule, 38 | MenuModule, 39 | MenubarModule, 40 | TabViewModule, 41 | InputTextareaModule, 42 | DynamicDialogModule, 43 | ConfirmPopupModule, 44 | DialogModule, 45 | TableModule, 46 | DropdownModule, 47 | ToastModule, 48 | ToolbarModule, 49 | ButtonModule, 50 | ], 51 | providers: [DialogService, MessageService, ConfirmationService], 52 | exports: [ 53 | FocusTrapModule, 54 | AutoFocusModule, 55 | InputTextModule, 56 | DividerModule, 57 | CheckboxModule, 58 | ScrollPanelModule, 59 | ColorPickerModule, 60 | SlideMenuModule, 61 | MenuModule, 62 | MenubarModule, 63 | TabViewModule, 64 | InputTextareaModule, 65 | DynamicDialogModule, 66 | ConfirmPopupModule, 67 | DialogModule, 68 | TableModule, 69 | DropdownModule, 70 | ToastModule, 71 | ToolbarModule, 72 | ButtonModule, 73 | ], 74 | }) 75 | export class PrimeModule {} 76 | -------------------------------------------------------------------------------- /frontend/src/app/shared/types/secure-string.ts: -------------------------------------------------------------------------------- 1 | export type SecureString = { 2 | value: string; 3 | }; 4 | 5 | export function isSecureString( 6 | obj: string | SecureString, 7 | ): obj is SecureString { 8 | return ( 9 | !(typeof obj === 'string') && (obj as SecureString).value !== undefined 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/shared/types/with-required.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility type that makes specified keys in a given type required. 3 | * 4 | * @template T - The original type. 5 | * @template K - The keys within T that should be required. 6 | * 7 | * @property {T} - The original type. 8 | * @property {Object} - An object where the specified keys are required. 9 | * 10 | * @example 11 | * // Define an interface with optional properties 12 | * interface User { 13 | * id?: number; 14 | * name?: string; 15 | * age?: number; 16 | * } 17 | * 18 | * // Use WithRequired to make 'id' and 'name' required 19 | * type UserWithRequiredIdAndName = WithRequired; 20 | * 21 | * // Now 'id' and 'name' are required, but 'age' is still optional 22 | * const user1: UserWithRequiredIdAndName = { 23 | * id: 1, 24 | * name: 'John Doe' 25 | * }; 26 | * 27 | * const user2: UserWithRequiredIdAndName = { 28 | * id: 2, 29 | * name: 'Jane Doe', 30 | * age: 30 31 | * }; 32 | * 33 | * // The following would cause a TypeScript error because 'id' and 'name' are required 34 | * const user3: UserWithRequiredIdAndName = { 35 | * name: 'John Doe' 36 | * }; 37 | * 38 | * @example 39 | * // Define another interface with optional properties 40 | * interface Product { 41 | * id?: string; 42 | * name?: string; 43 | * price?: number; 44 | * description?: string; 45 | * } 46 | * 47 | * // Use WithRequired to make 'id' required 48 | * type ProductWithRequiredId = WithRequired; 49 | * 50 | * // Now 'id' is required, but 'name', 'price', and 'description' are still optional 51 | * const product1: ProductWithRequiredId = { 52 | * id: 'abc123' 53 | * }; 54 | * 55 | * const product2: ProductWithRequiredId = { 56 | * id: 'xyz789', 57 | * name: 'Gadget', 58 | * price: 99.99, 59 | * description: 'A useful gadget' 60 | * }; 61 | * 62 | * // The following would cause a TypeScript error because 'id' is required 63 | * const product3: ProductWithRequiredId = { 64 | * name: 'Gadget', 65 | * price: 99.99 66 | * }; 67 | */ 68 | export type WithRequired = T & { [P in K]-?: T[P] }; 69 | -------------------------------------------------------------------------------- /frontend/src/app/shared/utils/word-wrap.spec.ts: -------------------------------------------------------------------------------- 1 | import { wordWrap } from './word-wrap'; 2 | 3 | describe('wordWrap', () => { 4 | it('should wrap a string to a given number of columns', () => { 5 | const input = 'The quick brown fox jumps over the lazy dog'; 6 | const cols = 10; 7 | const expectedOutput = 8 | 'The quick\r\nbrown fox\r\njumps over\r\nthe lazy\r\ndog'; 9 | expect(wordWrap(input, cols)).toBe(expectedOutput); 10 | }); 11 | 12 | it('should handle words longer than the column limit', () => { 13 | const input = 'A veryverylongword in the sentence'; 14 | const cols = 10; 15 | const expectedOutput = 'A\r\nveryverylongword\r\nin the\r\nsentence'; 16 | expect(wordWrap(input, cols)).toBe(expectedOutput); 17 | }); 18 | 19 | it('should handle empty string', () => { 20 | const input = ''; 21 | const cols = 10; 22 | const expectedOutput = ''; 23 | expect(wordWrap(input, cols)).toBe(expectedOutput); 24 | }); 25 | 26 | it('should handle single word shorter than the column limit', () => { 27 | const input = 'Word'; 28 | const cols = 10; 29 | const expectedOutput = 'Word'; 30 | expect(wordWrap(input, cols)).toBe(expectedOutput); 31 | }); 32 | 33 | it('should handle single word longer than the column limit', () => { 34 | const input = 'Supercalifragilisticexpialidocious'; 35 | const cols = 10; 36 | const expectedOutput = 'Supercalifragilisticexpialidocious'; 37 | expect(wordWrap(input, cols)).toBe(expectedOutput); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /frontend/src/app/shared/utils/word-wrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps a string to a given number of columns. 3 | * 4 | * @param {string} str - The string to be wrapped. 5 | * @param {number} cols - The maximum number of columns per line. 6 | * @returns {string} - The wrapped string. 7 | */ 8 | export function wordWrap(str: string, cols: number): string { 9 | let formattedString = ''; 10 | let currentLine = ''; 11 | 12 | // Split the input string into words 13 | const wordsArray = str.split(' '); 14 | 15 | // Iterate over each word 16 | for (let i = 0; i < wordsArray.length; i++) { 17 | const word = wordsArray[i]; 18 | 19 | // If the word itself is longer than the column limit 20 | if (word.length > cols) { 21 | // If the current line is not empty, add it to the formatted string 22 | if (currentLine.length > 0) { 23 | formattedString += currentLine + '\r\n'; 24 | currentLine = ''; 25 | } 26 | // Add the long word to the formatted string directly 27 | formattedString += word + '\r\n'; 28 | } else { 29 | // If adding the word exceeds the column limit 30 | if ( 31 | currentLine.length + word.length + (currentLine.length > 0 ? 1 : 0) > 32 | cols 33 | ) { 34 | formattedString += currentLine + '\r\n'; 35 | currentLine = word; 36 | } else { 37 | // Add the word to the current line 38 | currentLine += (currentLine.length > 0 ? ' ' : '') + word; 39 | } 40 | } 41 | } 42 | 43 | // Add any remaining text in the current line to the formatted string 44 | if (currentLine.length > 0) { 45 | formattedString += currentLine; 46 | } 47 | 48 | // Remove any trailing newline characters 49 | if (formattedString.endsWith('\r\n')) { 50 | formattedString = formattedString.slice(0, -2); 51 | } 52 | 53 | return formattedString; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/shared/window-config.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@angular/core'; 2 | 3 | export class WindowConfig { 4 | windowid = ''; // unique id 5 | parentWindow = ''; // id of the parent window, if any. 6 | visible = false; 7 | wtitle = 'Editor'; // Window title 8 | tooltip = ''; 9 | initalLock = false; // Initialisation of lock. 10 | save = false; // saving allowed or not. 11 | dontCancel = false; // supress cancelbutton 12 | component = 'EditorComponent'; 13 | zIndex = 0; 14 | tabID = ''; 15 | posx = 0; 16 | posy = 0; 17 | data?: any = undefined; 18 | winService?: any = undefined; 19 | outGoingEvents: EventEmitter = new EventEmitter(); 20 | inComingEvents: EventEmitter = new EventEmitter(); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-128x128.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-152x152.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-384x384.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-48x48.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-512x512.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-72x72.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/sb-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/sb-icon-96x96.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-128x128.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-152x152.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-384x384.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-48x48.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-512x512.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-72x72.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/unitopia-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/assets/icons/unitopia-icon-96x96.png -------------------------------------------------------------------------------- /frontend/src/environments/environment.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Environment { 2 | production: boolean; 3 | backendUrl: () => string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from './environment.interface'; 2 | 3 | export const environment: Environment = { 4 | production: true, 5 | backendUrl: () => window.location.origin, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | import { Environment } from './environment.interface'; 6 | 7 | export const environment: Environment = { 8 | production: false, 9 | // Change this to your local IP if you want to test on a mobile device in the same network 10 | backendUrl: () => "http://localhost:5000", 11 | }; 12 | 13 | /* 14 | * For easier debugging in development mode, you can import the following file 15 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 16 | * 17 | * This import should be commented out in production mode because it will have a negative impact 18 | * on performance if an error is thrown. 19 | */ 20 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 21 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitopia-de/webmud3/e44ab6346ad16913a3e83c61029d111d121667c9/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebMUD3 UNItopia 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err: unknown) => { 14 | console.error(err); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UNItopia-webmud3", 3 | "short_name": "UNItopia", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/unitopia-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/unitopia-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/unitopia-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/unitopia-icon-128x128.png", 30 | "sizes": "128x128", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/unitopia-icon-144x144.png", 36 | "sizes": "144x144", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/unitopia-icon-152x152.png", 42 | "sizes": "152x152", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/unitopia-icon-192x192.png", 48 | "sizes": "192x192", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/unitopia-icon-384x384.png", 54 | "sizes": "384x384", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | }, 58 | { 59 | "src": "assets/icons/unitopia-icon-512x512.png", 60 | "sizes": "512x512", 61 | "type": "image/png", 62 | "purpose": "maskable any" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~normalize.css"; 3 | 4 | html, 5 | body { 6 | height: 100%; 7 | width: 100%; 8 | font-size: 16px; 9 | } 10 | 11 | app-root { 12 | height: 100%; 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"], 10 | "exclude": ["src/environments/environment.prod.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "module": "es2020", 15 | "lib": ["es2018", "dom"], 16 | "useDefineForClassFields": false, 17 | "strict": true, 18 | "resolveJsonModule": true, 19 | "allowSyntheticDefaultImports": true, 20 | "paths": { 21 | "@mudlet3/frontend/core": ["src/app/core"], 22 | "@mudlet3/frontend/shared": ["src/app/shared"], 23 | "@mudlet3/frontend/features/ansi": ["src/app/features/ansi"], 24 | "@mudlet3/frontend/features/config": ["src/app/features/config"], 25 | "@mudlet3/frontend/features/files": ["src/app/features/files"], 26 | "@mudlet3/frontend/features/gmcp": ["src/app/features/gmcp"], 27 | "@mudlet3/frontend/features/modeless": ["src/app/features/modeless"], 28 | "@mudlet3/frontend/features/mudconfig": ["src/app/features/mudconfig"], 29 | "@mudlet3/frontend/features/settings": ["src/app/features/settings"], 30 | "@mudlet3/frontend/features/sockets": ["src/app/features/sockets"], 31 | "@mudlet3/frontend/features/widgets": ["src/app/features/widgets"] 32 | } 33 | }, 34 | "angularCompilerOptions": { 35 | "strictTemplates": true, 36 | "fullTemplateTypeCheck": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "module": "CommonJS", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ], 10 | "emitDecoratorMetadata": true 11 | }, 12 | "include": [ 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmud3", 3 | "version": "1.0.0-alpha", 4 | "engines": { 5 | "node": "20.12.2" 6 | }, 7 | "workspaces": [ 8 | "frontend", 9 | "backend" 10 | ], 11 | "scripts": { 12 | "build": "npm run build --workspaces", 13 | "build:prod": "npm run build:prod --workspace frontend && npm run build --workspace backend && npm run postbuild", 14 | "integrate-client": "ncp frontend/dist/frontend backend/dist/wwwroot", 15 | "postbuild": "npm run integrate-client", 16 | "start": "npm run build && npm run serve --workspace backend", 17 | "start:prod": "npm run build:prod && npm run serve --workspace backend", 18 | "test": "npm run test --workspaces", 19 | "lint": "npm run lint --workspaces" 20 | }, 21 | "devDependencies": { 22 | "ncp": "~2.0.0" 23 | } 24 | } 25 | --------------------------------------------------------------------------------