├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .mvn └── wrapper │ ├── .gitignore │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── LICENSE.md ├── README.md ├── docs ├── .gitkeep └── images │ ├── admin-login.JPG │ ├── file_upload.JPG │ ├── server-overview.JPG │ ├── server-settings.JPG │ ├── server-terminal.JPG │ └── server_configuration.JPG ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ ├── docker │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── buildconfig │ │ ├── cleanup.sh │ │ └── openttd.sh │ ├── java │ │ └── de │ │ │ └── litexo │ │ │ ├── AngularRouteFilter.java │ │ │ ├── OpenttdProcess.java │ │ │ ├── ProccesInputThread.java │ │ │ ├── ProcessExecutorService.java │ │ │ ├── ProcessOutputThread.java │ │ │ ├── ProcessThread.java │ │ │ ├── api │ │ │ ├── ChunkUploadResource.java │ │ │ ├── OpenttdServerResource.java │ │ │ ├── ServiceRuntimeException.java │ │ │ └── ServiceRuntimeExceptionMapper.java │ │ │ ├── commands │ │ │ ├── ClientsCommand.java │ │ │ ├── Command.java │ │ │ ├── PauseCommand.java │ │ │ ├── QuitCommand.java │ │ │ ├── ServerInfoCommand.java │ │ │ ├── UnpauseCommand.java │ │ │ └── model │ │ │ │ └── Client.java │ │ │ ├── events │ │ │ ├── BaseEvent.java │ │ │ ├── EventBus.java │ │ │ └── OpenttdTerminalUpdateEvent.java │ │ │ ├── model │ │ │ ├── external │ │ │ │ ├── BaseProcess.java │ │ │ │ ├── ExportModel.java │ │ │ │ ├── OpenttdServer.java │ │ │ │ ├── OpenttdServerConfigGet.java │ │ │ │ ├── OpenttdServerConfigUpdate.java │ │ │ │ ├── ServerFile.java │ │ │ │ ├── ServerFileType.java │ │ │ │ ├── ServiceError.java │ │ │ │ └── ServiceErrorType.java │ │ │ ├── internal │ │ │ │ └── InternalOpenttdServerConfig.java │ │ │ └── mapper │ │ │ │ ├── OpenttdServerConfigMapper.java │ │ │ │ ├── OpenttdServerMapper.java │ │ │ │ └── ServiceMapperConfiguration.java │ │ │ ├── repository │ │ │ └── DefaultRepository.java │ │ │ ├── scheduler │ │ │ ├── AutoPauseUnpause.java │ │ │ ├── Autosave.java │ │ │ ├── Housekeeping.java │ │ │ └── UpdateServerInfo.java │ │ │ ├── security │ │ │ ├── AuthResource.java │ │ │ ├── BasicAuth.java │ │ │ ├── BasicAuthSession.java │ │ │ ├── SecurityFiler.java │ │ │ ├── SecurityService.java │ │ │ └── SecurityUtils.java │ │ │ ├── services │ │ │ ├── ApplicationBootstrapService.java │ │ │ └── OpenttdService.java │ │ │ └── websocket │ │ │ └── DataStreamWebSocket.java │ └── resources │ │ ├── META-INF │ │ └── resources │ │ │ └── index.html │ │ ├── application.properties │ │ └── templates │ │ └── openttd-configs │ │ ├── openttd.cfg │ │ ├── private.cfg │ │ └── secrets.cfg └── test │ ├── java │ └── de │ │ └── litexo │ │ ├── TestCmd.java │ │ ├── commands │ │ ├── ClientsCommandTest.java │ │ ├── PauseCommandTest.java │ │ ├── ServerInfoCommandTest.java │ │ └── UnpauseCommandTest.java │ │ ├── repository │ │ └── DefaultRepositoryTest.java │ │ ├── scheduler │ │ ├── AutoPauseUnpauseTest.java │ │ └── HousekeepingTest.java │ │ └── services │ │ └── OpenttdServiceTest.java │ └── resources │ └── command-samples │ ├── clients.txt │ ├── pause.txt │ ├── serverInfo.txt │ └── unpause.txt └── ui ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── karma.conf.js ├── openapi └── service.yml ├── output ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── api │ │ ├── api-configuration.ts │ │ ├── api.module.ts │ │ ├── base-service.ts │ │ ├── models.ts │ │ ├── models │ │ │ ├── base-process.ts │ │ │ ├── command.ts │ │ │ ├── default-repository.ts │ │ │ ├── export-model.ts │ │ │ ├── internal-openttd-server-config.ts │ │ │ ├── openttd-process.ts │ │ │ ├── openttd-server-config-get.ts │ │ │ ├── openttd-server-config-update.ts │ │ │ ├── openttd-server-mapper.ts │ │ │ ├── openttd-server.ts │ │ │ ├── openttd-terminal-update-event.ts │ │ │ ├── path.ts │ │ │ ├── pause-command.ts │ │ │ ├── server-file-type.ts │ │ │ ├── server-file.ts │ │ │ ├── service-error-type.ts │ │ │ ├── service-error.ts │ │ │ └── unpause-command.ts │ │ ├── request-builder.ts │ │ ├── services.ts │ │ ├── services │ │ │ ├── auth-resource.service.ts │ │ │ ├── chunk-upload-resource.service.ts │ │ │ └── openttd-server-resource.service.ts │ │ └── strict-http-response.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ └── feature │ │ │ ├── login │ │ │ ├── login-routing.module.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.ts │ │ │ └── login.module.ts │ │ │ └── logout │ │ │ ├── logout.component.html │ │ │ ├── logout.component.scss │ │ │ ├── logout.component.ts │ │ │ └── logout.module.ts │ ├── servers │ │ ├── feature │ │ │ ├── servers-detail │ │ │ │ ├── servers-detail-routing.module.ts │ │ │ │ ├── servers-detail.component.html │ │ │ │ ├── servers-detail.component.scss │ │ │ │ ├── servers-detail.component.spec.ts │ │ │ │ ├── servers-detail.component.ts │ │ │ │ └── servers-detail.module.ts │ │ │ ├── servers-overview │ │ │ │ ├── servers-overview-routing.module.ts │ │ │ │ ├── servers-overview.component.html │ │ │ │ ├── servers-overview.component.scss │ │ │ │ ├── servers-overview.component.ts │ │ │ │ └── servers-overview.module.ts │ │ │ └── servers-shell │ │ │ │ ├── servers-shell-routing.module.ts │ │ │ │ └── servers-shell.module.ts │ │ └── ui │ │ │ ├── create-server-dialog │ │ │ ├── create-server-dialog.component.html │ │ │ ├── create-server-dialog.component.scss │ │ │ ├── create-server-dialog.component.ts │ │ │ └── create-server-dialog.module.ts │ │ │ ├── openttd-process-terminal │ │ │ ├── openttd-process-terminal-dialog.component.html │ │ │ ├── openttd-process-terminal-dialog.component.scss │ │ │ ├── openttd-process-terminal-dialog.component.ts │ │ │ └── openttd-process-terminal-dialog.module.ts │ │ │ └── openttd-server-grid │ │ │ ├── openttd-server-grid.component.html │ │ │ ├── openttd-server-grid.component.scss │ │ │ ├── openttd-server-grid.component.spec.ts │ │ │ ├── openttd-server-grid.component.ts │ │ │ └── openttd-server-grid.module.ts │ ├── services │ ├── settings │ │ ├── feature │ │ │ └── settings │ │ │ │ ├── settings-routing.module.ts │ │ │ │ ├── settings.component.html │ │ │ │ ├── settings.component.scss │ │ │ │ ├── settings.component.spec.ts │ │ │ │ ├── settings.component.ts │ │ │ │ └── settings.module.ts │ │ └── ui │ │ │ └── .gitkeep │ └── shared │ │ ├── guards │ │ └── auth.guard.ts │ │ ├── interceptors │ │ ├── custom-loading-bar.interceptor.ts │ │ └── http-auth-interceptor.ts │ │ ├── model │ │ └── constants.ts │ │ ├── services │ │ ├── application.service.ts │ │ ├── authentication.service.ts │ │ ├── backend-websocket.service.ts │ │ └── utils.service.ts │ │ ├── store │ │ ├── actions │ │ │ └── app.actions.ts │ │ ├── effects │ │ │ └── app.effects.ts │ │ ├── reducers │ │ │ ├── app.reducer.ts │ │ │ └── index.ts │ │ └── selectors │ │ │ └── app.selectors.ts │ │ └── ui │ │ ├── app-notifications │ │ ├── app-notifications.component.html │ │ ├── app-notifications.component.scss │ │ ├── app-notifications.component.spec.ts │ │ ├── app-notifications.component.ts │ │ └── app-notifications.module.ts │ │ ├── base-dialog │ │ ├── base-dialog.component.html │ │ ├── base-dialog.component.scss │ │ ├── base-dialog.component.spec.ts │ │ ├── base-dialog.component.ts │ │ └── base-dialog.module.ts │ │ ├── file-upload-dialog │ │ ├── file-upload-dialog.component.html │ │ ├── file-upload-dialog.component.scss │ │ ├── file-upload-dialog.component.spec.ts │ │ ├── file-upload-dialog.component.ts │ │ └── file-upload-dialog.module.ts │ │ ├── server-file-select │ │ ├── server-file-select.component.html │ │ ├── server-file-select.component.scss │ │ ├── server-file-select.component.ts │ │ └── server-file-select.module.ts │ │ ├── sidebar-layout │ │ ├── sidebar-layout.component.html │ │ ├── sidebar-layout.component.scss │ │ ├── sidebar-layout.component.spec.ts │ │ ├── sidebar-layout.component.ts │ │ └── sidebar-layout.module.ts │ │ └── terminal │ │ ├── terminal.component.html │ │ ├── terminal.component.scss │ │ ├── terminal.component.spec.ts │ │ ├── terminal.component.ts │ │ └── terminal.module.ts ├── assets │ ├── .gitkeep │ └── images │ │ ├── admin.PNG │ │ ├── openttd-server-logo.PNG │ │ └── server-logo-default.PNG ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json ├── update-api.ps1 └── update-api.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build OpenTTD Server 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: . 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | - name: Build with Maven 23 | run: mvn --batch-mode --update-snapshots verify 24 | - name: Log in to Docker Hub 25 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 32 | with: 33 | images: hauschi86/openttd-server 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 36 | with: 37 | context: . 38 | file: src/main/docker/Dockerfile 39 | push: true 40 | tags: ${{ steps.meta.outputs.tags }} 41 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release OpenTTD Server 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: . 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '17' 20 | distribution: 'adopt' 21 | - name: Build with Maven 22 | run: mvn --batch-mode --update-snapshots verify 23 | - name: Log in to Docker Hub 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | - name: Extract metadata (tags, labels) for Docker 29 | id: meta 30 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 31 | with: 32 | images: hauschi86/openttd-server 33 | - name: Build and push Docker image 34 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 35 | with: 36 | context: . 37 | file: src/main/docker/Dockerfile 38 | push: true 39 | tags: ${{ steps.meta.outputs.tags }} 40 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .project 3 | .classpath 4 | .settings/ 5 | bin/ 6 | testapikeys.properties 7 | 8 | # IntelliJ 9 | .idea 10 | *.ipr 11 | *.iml 12 | *.iws 13 | 14 | # NetBeans 15 | nb-configuration.xml 16 | 17 | # Visual Studio Code 18 | .vscode 19 | .factorypath 20 | 21 | # OSX 22 | .DS_Store 23 | 24 | # Vim 25 | *.swp 26 | *.swo 27 | 28 | # patch 29 | *.orig 30 | *.rej 31 | 32 | # Maven 33 | target/ 34 | pom.xml.tag 35 | pom.xml.releaseBackup 36 | pom.xml.versionsBackup 37 | release.properties 38 | server 39 | out 40 | -------------------------------------------------------------------------------- /.mvn/wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | maven-wrapper.jar 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/.gitkeep -------------------------------------------------------------------------------- /docs/images/admin-login.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/admin-login.JPG -------------------------------------------------------------------------------- /docs/images/file_upload.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/file_upload.JPG -------------------------------------------------------------------------------- /docs/images/server-overview.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/server-overview.JPG -------------------------------------------------------------------------------- /docs/images/server-settings.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/server-settings.JPG -------------------------------------------------------------------------------- /docs/images/server-terminal.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/server-terminal.JPG -------------------------------------------------------------------------------- /docs/images/server_configuration.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/docs/images/server_configuration.JPG -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | #lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonIgnore 2 | #lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty 3 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ARG OPENTTD_VERSION="14.1" 4 | ARG OPENGFX_VERSION="0.7.1" 5 | 6 | RUN apt-get update 7 | RUN apt-get install dos2unix 8 | RUN apt-get install -y wget 9 | RUN apt-get install libgomp1 10 | 11 | # See: https://jdk.java.net/archive/ 12 | ENV JDK_DOWNLOAD https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz 13 | 14 | # Install OpenJDK Java 17 SDK 15 | ENV JVM_DIR /usr/lib/jvm 16 | RUN mkdir -p "${JVM_DIR}" 17 | 18 | 19 | RUN wget -q --no-cookies --no-check-certificate \ 20 | -O "${DOWNLOAD_DIR}/openjdk-17.0.2_linux-x64_bin.tar.gz" "${JDK_DOWNLOAD}" \ 21 | && cd "${JVM_DIR}" \ 22 | && tar --no-same-owner -xzf "${DOWNLOAD_DIR}/openjdk-17.0.2_linux-x64_bin.tar.gz" \ 23 | && rm -f "${DOWNLOAD_DIR}/openjdk-17.0.2_linux-x64_bin.tar.gz" \ 24 | && mv "${JVM_DIR}/jdk-17.0.2" "${JVM_DIR}/java-17.0.2-openjdk-x64" \ 25 | && ln -s "${JVM_DIR}/java-17.0.2-openjdk-x64" "${JVM_DIR}/java-17-openjdk-x64" 26 | 27 | ENV JAVA_HOME ${JVM_DIR}/java-17-openjdk-x64 28 | 29 | ENV PATH=$PATH:$HOME/bin:$JAVA_HOME/bin 30 | 31 | COPY src/main/docker/cleanup.sh /tmp/cleanup.sh 32 | COPY src/main/docker/buildconfig /tmp/buildconfig 33 | COPY --chown=1000:1000 src/main/docker/openttd.sh /openttd.sh 34 | 35 | RUN dos2unix /tmp/cleanup.sh 36 | RUN dos2unix /tmp/buildconfig 37 | RUN dos2unix /openttd.sh 38 | 39 | RUN chmod +x /tmp/cleanup.sh /openttd.sh 40 | 41 | RUN apt update -qq 42 | RUN apt install -yqq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef dumb-init wget unzip ca-certificates libfontconfig1 libfreetype6 libfluidsynth2 libicu-dev libpng16-16 liblzma-dev liblzo2-2 libsdl1.2debian libsdl2-2.0-0 xz-utils > /dev/null 2>&1 43 | 44 | 45 | ## Create user 46 | RUN adduser --uid 1000 --shell /bin/bash --gecos "" openttd 47 | RUN addgroup openttd users 48 | RUN passwd -d openttd 49 | 50 | USER openttd 51 | WORKDIR /home/openttd 52 | RUN wget https://cdn.openttd.org/openttd-releases/14.1/openttd-14.1-linux-generic-amd64.tar.xz 53 | RUN wget https://cdn.openttd.org/opengfx-releases/7.1/opengfx-7.1-all.zip 54 | 55 | RUN tar -xf openttd-14.1-linux-generic-amd64.tar.xz 56 | 57 | RUN mkdir openttd-14 58 | RUN cp -a openttd-14.1-linux-generic-amd64/. openttd-14 59 | 60 | RUN unzip opengfx-7.1-all.zip 61 | RUN tar -xf opengfx-7.1.tar -C openttd-14/baseset/ 62 | 63 | RUN rm -rf opengfx-*.tar opengfx-*.zip openttd-14.1* 64 | ## Set entrypoint script to right user 65 | RUN chmod +x /openttd.sh 66 | 67 | 68 | 69 | USER root 70 | RUN /tmp/cleanup.sh 71 | 72 | RUN mkdir /home/openttd/server 73 | RUN chown 1000:1000 /home/openttd/server 74 | 75 | VOLUME /home/openttd 76 | 77 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' 78 | 79 | # We make four distinct layers so if there are application changes the library layers can be re-used 80 | COPY --chown=1000 target/quarkus-app/lib/ /deployments/lib/ 81 | COPY --chown=1000 target/quarkus-app/*.jar /deployments/ 82 | COPY --chown=1000 target/quarkus-app/app/ /deployments/app/ 83 | COPY --chown=1000 target/quarkus-app/quarkus/ /deployments/quarkus/ 84 | 85 | USER 1000 86 | 87 | ENV start-server.command="/openttd.sh" 88 | 89 | ENV openttd.save.dir=/home/openttd/server/save 90 | ENV openttd.config.dir=/home/openttd/server/config 91 | ENV server.config.dir=/home/openttd/server 92 | 93 | CMD ["java", "-jar", "/deployments/quarkus-run.jar"] 94 | -------------------------------------------------------------------------------- /src/main/docker/buildconfig: -------------------------------------------------------------------------------- 1 | export LC_ALL=C 2 | export DEBIAN_FRONTEND=noninteractive 3 | 4 | minimal_apt_get_install='apt install -yqq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef' 5 | -------------------------------------------------------------------------------- /src/main/docker/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | apt-get remove -yqq unzip wget 6 | apt-get autoremove -yqq 7 | apt-get autoclean -yqq 8 | 9 | rm -rf /var/lib/apt/lists/* 10 | rm -f /etc/dpkg/dpkg.cfg.d/02apt-speedup 11 | rm -f /etc/ssh/ssh_host_* 12 | rm -rf /tmp/* /var/tmp/* 13 | -------------------------------------------------------------------------------- /src/main/docker/openttd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PUID=${PUID:-911} 3 | PGID=${PGID:-911} 4 | PHOME=${PHOME:-"/home/openttd"} 5 | USER=${USER:-"openttd"} 6 | 7 | if [ ! "$(id -u ${USER})" -eq "$PUID" ]; then usermod -o -u "$PUID" ${USER} ; fi 8 | if [ ! "$(id -g ${USER})" -eq "$PGID" ]; then groupmod -o -g "$PGID" ${USER} ; fi 9 | if [ "$(grep ${USER} /etc/passwd | cut -d':' -f6)" != "${PHOME}" ]; then 10 | if [ ! -d ${PHOME} ]; then 11 | mkdir -p ${PHOME} 12 | chown ${USER}:${USER} ${PHOME} 13 | fi 14 | usermod -m -d ${PHOME} ${USER} 15 | fi 16 | 17 | echo " 18 | ----------------------------------- 19 | GID/UID 20 | ----------------------------------- 21 | User uid: $(id -u ${USER}) 22 | User gid: $(id -g ${USER}) 23 | User Home: $(grep ${USER} /etc/passwd | cut -d':' -f6) 24 | ----------------------------------- 25 | " 26 | 27 | cmd="" 28 | for var in "$@" 29 | do 30 | cmd="$cmd '$var' " 31 | done 32 | 33 | su -l openttd -c "/home/openttd/openttd-14/openttd -D ${cmd}" 34 | exit 0 35 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/AngularRouteFilter.java: -------------------------------------------------------------------------------- 1 | package de.litexo; 2 | 3 | import javax.servlet.FilterChain; 4 | import javax.servlet.ServletException; 5 | import javax.servlet.ServletRequest; 6 | import javax.servlet.ServletResponse; 7 | import javax.servlet.annotation.WebFilter; 8 | import javax.servlet.http.HttpFilter; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | import java.io.IOException; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * Filter for routing all 404-Errors to angular and trigger its router * 17 | 18 | */ 19 | @WebFilter(urlPatterns = "/*") 20 | public class AngularRouteFilter extends HttpFilter { 21 | 22 | private static final Pattern FILE_NAME_PATTERN = Pattern.compile(".*[.][a-zA-Z\\d]+"); 23 | 24 | @Override 25 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 26 | 27 | HttpServletRequest request = (HttpServletRequest) req; 28 | HttpServletResponse response = (HttpServletResponse) res; 29 | chain.doFilter(request, response); 30 | 31 | if (response.getStatus() == 404) { 32 | String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", ""); 33 | if (!FILE_NAME_PATTERN.matcher(path).matches()) { 34 | // We could not find the resource, i.e. it is not anything known to the server (i.e. it is not a REST 35 | // endpoint or a servlet), and does not look like a file so try handling it in the front-end routes 36 | // and reset the response status code to 200. 37 | response.reset(); 38 | request.getRequestDispatcher("/").forward(request, response); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/ProccesInputThread.java: -------------------------------------------------------------------------------- 1 | package de.litexo; 2 | 3 | import de.litexo.api.ServiceRuntimeException; 4 | 5 | import java.util.LinkedList; 6 | import java.util.Queue; 7 | 8 | public class ProccesInputThread implements Runnable { 9 | 10 | private final StringBuilder console; 11 | Process process; 12 | 13 | 14 | private Queue data = new LinkedList<>(); 15 | 16 | private boolean stopped = false; 17 | 18 | public ProccesInputThread(Process process, StringBuilder console) { 19 | this.process = process; 20 | this.console = console; 21 | } 22 | 23 | @Override 24 | public void run() { 25 | try { 26 | while (!stopped) { 27 | if (!data.isEmpty()) { 28 | String dataValue = data.poll(); 29 | if (dataValue != null) { 30 | dataValue += "\n"; 31 | process.getOutputStream().write(dataValue.getBytes()); 32 | process.getOutputStream().flush(); 33 | console.append(dataValue + "\r\n"); 34 | } 35 | 36 | } 37 | Thread.sleep(100); 38 | } 39 | 40 | } catch (InterruptedException e) { 41 | Thread.currentThread().interrupt(); 42 | } catch (Exception e) { 43 | throw new ServiceRuntimeException(e); 44 | } 45 | System.out.println("Finished: ProccesInputThread"); 46 | } 47 | 48 | public void write(String data) { 49 | if (data != null) { 50 | this.data.add(data); 51 | } 52 | } 53 | 54 | public void stop() { 55 | this.stopped = true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/ProcessExecutorService.java: -------------------------------------------------------------------------------- 1 | package de.litexo; 2 | 3 | 4 | import de.litexo.api.ServiceRuntimeException; 5 | import de.litexo.events.EventBus; 6 | import org.eclipse.microprofile.context.ManagedExecutor; 7 | 8 | import javax.enterprise.context.ApplicationScoped; 9 | import javax.inject.Inject; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | @ApplicationScoped 15 | public class ProcessExecutorService { 16 | @Inject 17 | ManagedExecutor executor; 18 | 19 | @Inject 20 | EventBus eventBus; 21 | 22 | Map processes = new HashMap<>(); 23 | 24 | public ProcessThread start(List command) { 25 | ProcessThread processThread = new ProcessThread(this.executor, command, eventBus); 26 | this.executor.execute(processThread); 27 | return processThread; 28 | } 29 | 30 | public void write(String processId, String data) { 31 | if (this.processes.containsKey(processId)) { 32 | this.processes.get(processId).write(data); 33 | } else { 34 | throw new ServiceRuntimeException("Failed to write to process with id: " + processId); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/ProcessOutputThread.java: -------------------------------------------------------------------------------- 1 | package de.litexo; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStream; 5 | import java.io.InputStreamReader; 6 | 7 | public class ProcessOutputThread implements Runnable { 8 | InputStream processInputStream; 9 | 10 | private StringBuilder data; 11 | 12 | private boolean stopped = false; 13 | 14 | public ProcessOutputThread(InputStream processInputStream, StringBuilder data) { 15 | this.processInputStream = processInputStream; 16 | this.data = data; 17 | } 18 | 19 | @Override 20 | public void run() { 21 | try { 22 | 23 | BufferedReader inputReader = new BufferedReader(new InputStreamReader(processInputStream)); 24 | 25 | while (!this.stopped) { 26 | for (int ch; (ch = inputReader.read()) != -1; ) { 27 | data.append((char) ch); 28 | } 29 | Thread.sleep(50); 30 | } 31 | System.out.println("Finished: ProcessOutputThread"); 32 | } catch (Exception e) { 33 | Thread.currentThread().interrupt(); 34 | } 35 | } 36 | 37 | public void stop() { 38 | this.stopped = true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/api/ChunkUploadResource.java: -------------------------------------------------------------------------------- 1 | package de.litexo.api; 2 | 3 | import de.litexo.model.external.ServerFileType; 4 | import org.eclipse.microprofile.config.inject.ConfigProperty; 5 | import org.jboss.resteasy.spi.HttpRequest; 6 | 7 | import javax.annotation.PostConstruct; 8 | import javax.annotation.security.RolesAllowed; 9 | import javax.ws.rs.Consumes; 10 | import javax.ws.rs.POST; 11 | import javax.ws.rs.QueryParam; 12 | import javax.ws.rs.core.Context; 13 | import javax.ws.rs.core.HttpHeaders; 14 | import javax.ws.rs.core.MediaType; 15 | import javax.ws.rs.core.Response; 16 | import java.io.IOException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.nio.file.StandardOpenOption; 21 | 22 | @javax.ws.rs.Path("/api/chunk-upload") 23 | @RolesAllowed("login_user") 24 | public class ChunkUploadResource { 25 | @ConfigProperty(name = "openttd.save.dir") 26 | String openttdSaveDir; 27 | 28 | @ConfigProperty(name = "openttd.config.dir") 29 | String openttdConfigDir; 30 | 31 | Path configDir; 32 | 33 | Path saveDir; 34 | 35 | @PostConstruct 36 | void init() throws IOException { 37 | this.configDir = initDir(this.openttdConfigDir); 38 | this.saveDir = initDir(this.openttdSaveDir); 39 | } 40 | 41 | private Path initDir(String path) throws IOException { 42 | if (!Files.exists(Paths.get(path))) { 43 | return Files.createDirectories(Paths.get(path)); 44 | } else { 45 | return Paths.get(path); 46 | } 47 | } 48 | 49 | /** 50 | * Endpoint for chunked file upload 51 | * 52 | * @param fileName name of the file 53 | * @param offset offset in bytes of the current chunk 54 | * @param size total size of the file that is sent 55 | * @param chunk chunk of bytes of the transmitted file 56 | * @param request http request object (inject by container) 57 | * @return response 58 | * @throws IOException 59 | */ 60 | @POST 61 | @Consumes(MediaType.APPLICATION_OCTET_STREAM) 62 | public Response add(@QueryParam("type") ServerFileType type, @QueryParam("fileName") String fileName, 63 | @QueryParam("offset") int offset, @QueryParam("fileSize") int size, byte[] chunk, 64 | @Context HttpRequest request) throws IOException { 65 | System.out.println(String.format("name: %s from:%s to:%s size:%s Auth:Header %s" 66 | , fileName, offset, offset + chunk.length, size, request.getHttpHeaders().getHeaderString(HttpHeaders.AUTHORIZATION))); 67 | 68 | if (appendWrite(type, fileName, size, offset, chunk)) { 69 | return Response.status(201).build(); 70 | } 71 | return Response.ok().build(); 72 | } 73 | 74 | private boolean appendWrite(ServerFileType type, String fileName, int size, int offset, byte[] chunk) throws IOException { 75 | Path upload = null; 76 | switch (type) { 77 | case CONFIG -> upload = configDir.resolve(fileName); 78 | case SAVE_GAME -> upload = saveDir.resolve(fileName); 79 | default -> throw new ServiceRuntimeException("Unknown file type for upload"); 80 | 81 | } 82 | 83 | if (Files.exists(upload) && offset == 0) { 84 | Files.deleteIfExists(upload); 85 | } 86 | 87 | if (!Files.exists(upload) || offset == 0) { 88 | Files.write(upload, new byte[0]); 89 | } 90 | 91 | Files.write(upload, chunk, StandardOpenOption.APPEND); 92 | 93 | long uploaded = Files.size(upload); 94 | System.out.println("Size on server: " + uploaded + " Expected: " + size + "Appended: " + chunk.length); 95 | 96 | return uploaded == size; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/api/ServiceRuntimeException.java: -------------------------------------------------------------------------------- 1 | package de.litexo.api; 2 | 3 | public class ServiceRuntimeException extends RuntimeException { 4 | public ServiceRuntimeException(String message) { 5 | super(message); 6 | } 7 | 8 | public ServiceRuntimeException(String message, Throwable cause) { 9 | super(message, cause); 10 | } 11 | 12 | public ServiceRuntimeException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/api/ServiceRuntimeExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package de.litexo.api; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import de.litexo.model.external.ServiceError; 6 | import de.litexo.repository.DefaultRepository; 7 | import org.apache.commons.lang3.exception.ExceptionUtils; 8 | import org.jboss.logging.Logger; 9 | 10 | import javax.ws.rs.core.MediaType; 11 | import javax.ws.rs.core.Response; 12 | import javax.ws.rs.ext.ExceptionMapper; 13 | import javax.ws.rs.ext.Provider; 14 | 15 | import static de.litexo.model.external.ServiceErrorType.RUNTIME_EXCEPTION; 16 | import static javax.ws.rs.core.Response.status; 17 | 18 | @Provider 19 | public class ServiceRuntimeExceptionMapper implements ExceptionMapper { 20 | private static final Logger LOG = Logger.getLogger(ServiceRuntimeExceptionMapper.class); 21 | 22 | private static String toJson(final Object result) throws JsonProcessingException { 23 | 24 | return new ObjectMapper().writeValueAsString(result); 25 | } 26 | 27 | @Override 28 | public Response toResponse(final ServiceRuntimeException ex) { 29 | 30 | try { 31 | final ServiceError se = new ServiceError(); 32 | se.setType(RUNTIME_EXCEPTION); 33 | se.setMessage(ex.getMessage()); 34 | se.setStackTrace(ExceptionUtils.getStackTrace(ex)); 35 | return status(500).type(MediaType.APPLICATION_JSON).entity(toJson(se)).build(); 36 | } catch (final JsonProcessingException e) { 37 | LOG.error("Error while exception mapping", e); 38 | return status(500).build(); 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/ClientsCommand.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.commands.model.Client; 4 | import lombok.Getter; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class ClientsCommand extends Command { 13 | 14 | @Getter 15 | private List clients = new ArrayList<>(); 16 | 17 | public ClientsCommand() { 18 | super("clients"); 19 | } 20 | 21 | @Override 22 | public boolean check(String logs) { 23 | List lines = new ArrayList<>(Arrays.asList(logs.split("\\R"))); 24 | List found = new ArrayList<>(); 25 | for (String line : lines) { 26 | if (line.contains("Client #")) { 27 | found.add(parseClient(line)); 28 | } 29 | } 30 | 31 | // Read until no changes 32 | if (found.isEmpty() || found.size() > this.clients.size()) { 33 | this.clients = found; 34 | return false; 35 | } else { 36 | this.clients = found; 37 | return true; 38 | } 39 | } 40 | 41 | private Client parseClient(String line) { 42 | Client client = new Client(); 43 | String name = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'")); 44 | String withoutName = line.substring(0, line.indexOf("name") - 1) + line.substring(line.lastIndexOf("'") + 1); 45 | String regex = "(?ims)^Client\s+#([0-9]+)\s+company:\s+([0-9]+)\s+IP:\s+([a-zA-z0-9.:]+)[^\s]*$"; 46 | Pattern p = Pattern.compile(regex); 47 | 48 | Matcher m = p.matcher(withoutName); 49 | 50 | if (m.find()) { 51 | client.setName(name); 52 | client.setIndex(m.group(1)); 53 | client.setCompany(m.group(2)); 54 | client.setIp(m.group(3)); 55 | } 56 | System.out.println(client); 57 | return client; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/Command.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import de.litexo.ProcessThread; 6 | import de.litexo.api.ServiceRuntimeException; 7 | import lombok.Getter; 8 | 9 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type") 10 | @JsonSubTypes({ 11 | @JsonSubTypes.Type(value = PauseCommand.class, name = "PauseCommand"), 12 | @JsonSubTypes.Type(value = UnpauseCommand.class, name = "UnpauseCommand"), 13 | @JsonSubTypes.Type(value = ServerInfoCommand.class, name = "ServerInfoCommand"), 14 | @JsonSubTypes.Type(value = ClientsCommand.class, name = "ClientsCommand") 15 | }) 16 | public abstract class Command { 17 | 18 | String marker; 19 | String command; 20 | String rawResult; 21 | 22 | @Getter 23 | boolean executed = false; 24 | 25 | boolean cmdSend = false; 26 | 27 | protected Command(String command) { 28 | this.command = command; 29 | this.marker = "@@@@_" + getType() + "_" + System.currentTimeMillis() + "_@@@@"; 30 | } 31 | 32 | public String getType() { 33 | return getClass().getSimpleName(); 34 | } 35 | 36 | 37 | public abstract boolean check(String logs); 38 | 39 | /** 40 | * Will be called if the command was successfully executed 41 | * 42 | * @param openttdServeId id of server that executed the command 43 | */ 44 | public void onSuccess(String openttdServeId){ 45 | 46 | } 47 | 48 | public Command execute(ProcessThread process, String openttdServerId ) { 49 | process.write(this.marker); 50 | for (int i = 0; i < 100; i++) { 51 | try { 52 | Thread.sleep(200L); 53 | if (!cmdSend) { 54 | process.write(this.command); 55 | this.cmdSend = true; 56 | } 57 | } catch (InterruptedException e) { 58 | Thread.currentThread().interrupt(); 59 | } 60 | int beginIndex = process.getLogs().indexOf(this.marker); 61 | if (beginIndex == -1) { 62 | continue; 63 | } 64 | this.rawResult = process.getLogs().substring(beginIndex); 65 | if (check(this.rawResult)) { 66 | this.executed = true; 67 | this.onSuccess(openttdServerId); 68 | return this; 69 | } 70 | } 71 | return this; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/PauseCommand.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.model.external.OpenttdServer; 4 | import de.litexo.repository.DefaultRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public class PauseCommand extends Command { 9 | 10 | private final DefaultRepository repository; 11 | 12 | public PauseCommand(DefaultRepository repository) { 13 | super("pause"); 14 | this.repository = repository; 15 | } 16 | 17 | @Override 18 | public boolean check(String logs) { 19 | return logs.contains("*** Game paused (manual)") || logs.contains("Game is already paused"); 20 | } 21 | 22 | @Override 23 | public void onSuccess(String openttdServeId) { 24 | 25 | Optional openttdServer = this.repository.getOpenttdServer(openttdServeId); 26 | if (openttdServer.isPresent()) { 27 | openttdServer.get().setPaused(true); 28 | this.repository.updateServer(openttdServeId, openttdServer.get()); 29 | } 30 | super.onSuccess(openttdServeId); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/QuitCommand.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | public class QuitCommand extends Command { 4 | 5 | public QuitCommand() { 6 | super("quit"); 7 | } 8 | 9 | @Override 10 | public boolean check(String logs) { 11 | return logs.contains("quit"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/ServerInfoCommand.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | public class ServerInfoCommand extends Command { 10 | 11 | @Getter 12 | private String inviteCode; 13 | @Getter 14 | private int currentClients; 15 | @Getter 16 | private int maxClients; 17 | @Getter 18 | private int currentCompanies; 19 | @Getter 20 | private int maxCompanies; 21 | @Getter 22 | private int currentSpectators; 23 | 24 | public ServerInfoCommand() { 25 | super("server_info"); 26 | } 27 | 28 | @Override 29 | public boolean check(String logs) { 30 | List lines = new ArrayList<>(Arrays.asList(logs.split("\\R"))); 31 | int matched = 0; 32 | for (String line : lines) { 33 | if (line.contains("Invite code:")) { 34 | String[] split = line.split("Invite code:"); 35 | if (split.length > 1) { 36 | this.inviteCode = split[1].trim(); 37 | } 38 | matched++; 39 | } 40 | if (line.contains("Current/maximum clients:")) { 41 | String[] split = line.split("Current/maximum clients:"); 42 | if (split.length > 1) { 43 | this.currentClients = Integer.parseInt(split[1].split("/")[0].trim()); 44 | this.maxClients = Integer.parseInt(split[1].split("/")[1].trim()); 45 | } 46 | matched++; 47 | } 48 | if (line.contains("Current/maximum companies:")) { 49 | String[] split = line.split("Current/maximum companies:"); 50 | if (split.length > 1) { 51 | this.currentCompanies = Integer.parseInt(split[1].split("/")[0].trim()); 52 | this.maxCompanies = Integer.parseInt(split[1].split("/")[1].trim()); 53 | } 54 | matched++; 55 | } 56 | if (line.contains("Current spectators:")) { 57 | String[] split = line.split("Current spectators:"); 58 | if (split.length > 1) { 59 | this.currentSpectators = Integer.parseInt(split[1].trim()); 60 | } 61 | matched++; 62 | } 63 | 64 | } 65 | return matched == 4; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/UnpauseCommand.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.model.external.OpenttdServer; 4 | import de.litexo.repository.DefaultRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public class UnpauseCommand extends Command { 9 | 10 | private final DefaultRepository repository; 11 | 12 | public UnpauseCommand(DefaultRepository repository) { 13 | super("unpause"); 14 | this.repository = repository; 15 | } 16 | 17 | @Override 18 | public boolean check(String logs) { 19 | return logs.contains("*** Game unpaused (manual)") || logs.contains("Game is already unpaused"); 20 | } 21 | 22 | @Override 23 | public void onSuccess(String openttdServeId) { 24 | Optional openttdServer = this.repository.getOpenttdServer(openttdServeId); 25 | if (openttdServer.isPresent()) { 26 | openttdServer.get().setPaused(false); 27 | repository.updateServer(openttdServeId, openttdServer.get()); 28 | } 29 | super.onSuccess(openttdServeId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/commands/model/Client.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Client { 7 | private String index; 8 | private String name; 9 | private String company; 10 | private String ip; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/events/BaseEvent.java: -------------------------------------------------------------------------------- 1 | package de.litexo.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.ToString; 8 | 9 | /** 10 | * @author Andreas Hauschild 11 | */ 12 | @ToString 13 | @JsonIgnoreProperties(ignoreUnknown = true) 14 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type") 15 | public abstract class BaseEvent { 16 | 17 | private long created = System.currentTimeMillis(); 18 | 19 | private final Class clazz; 20 | 21 | protected BaseEvent(Object eventSource) { 22 | this.clazz = eventSource.getClass(); 23 | } 24 | 25 | public String getSource() { 26 | return this.clazz.getCanonicalName(); 27 | } 28 | 29 | public long getCreated() { 30 | return created; 31 | } 32 | 33 | public String toJson() { 34 | try { 35 | return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this); 36 | } catch (JsonProcessingException e) { 37 | e.printStackTrace(); 38 | } 39 | return null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/events/OpenttdTerminalUpdateEvent.java: -------------------------------------------------------------------------------- 1 | package de.litexo.events; 2 | 3 | import lombok.Getter; 4 | import lombok.ToString; 5 | 6 | @ToString(callSuper = true) 7 | 8 | public class OpenttdTerminalUpdateEvent extends BaseEvent { 9 | 10 | @Getter 11 | private String processId; 12 | 13 | @Getter 14 | private String text; 15 | 16 | public OpenttdTerminalUpdateEvent(Object eventSource, String processId, String text) { 17 | super(eventSource); 18 | this.processId = processId; 19 | this.text = text; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/BaseProcess.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | @Data 7 | @Accessors(chain = true) 8 | public class BaseProcess { 9 | private String processId; 10 | private String processData; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/ExportModel.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import de.litexo.commands.PauseCommand; 4 | import de.litexo.commands.UnpauseCommand; 5 | import de.litexo.events.OpenttdTerminalUpdateEvent; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class ExportModel { 10 | private OpenttdTerminalUpdateEvent openttdTerminalUpdateEvent; 11 | private UnpauseCommand unpauseCommand; 12 | private PauseCommand pauseCommand; 13 | 14 | private ServiceError serviceError; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/OpenttdServer.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import de.litexo.OpenttdProcess; 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | import java.util.UUID; 10 | 11 | @Data 12 | @Accessors(chain = true) 13 | @JsonIgnoreProperties(ignoreUnknown = true) 14 | public class OpenttdServer { 15 | 16 | // unique id of the erver 17 | private String id = UUID.randomUUID().toString(); 18 | 19 | // Name of the server 20 | private String name = null; 21 | 22 | // Password of the server. 23 | private String password = null; 24 | 25 | // Password for admin endpoint - admin_password 26 | private String adminPassword = null; 27 | 28 | // Port of the server admin 29 | private Integer serverAdminPort = null; 30 | 31 | // of the server 32 | private Integer port = null; 33 | 34 | // the save game that is loaded by default. this will be set on application startup to the newest save game that belongs to this server on 35 | private ServerFile saveGame = null; 36 | 37 | // if set this config will be used to start the server 38 | private ServerFile openttdConfig = null; 39 | 40 | 41 | // if set this is the private config that will be used to start the server 42 | private ServerFile openttdPrivateConfig = null; 43 | 44 | // if set this is used to set the secret config of the server 45 | private ServerFile openttdSecretsConfig = null; 46 | 47 | // Enables auto save for this server 48 | private boolean autoSave = true; 49 | 50 | // Enables autoPause for this server if no played has joined a company 51 | private boolean autoPause = true; 52 | 53 | // State flag that is set if a pause action was executed 54 | private boolean paused = false; 55 | 56 | // Server Info Command result 57 | private String inviteCode; 58 | 59 | // Server Info Command result 60 | private int currentClients; 61 | 62 | // Server Info Command result 63 | private int maxClients; 64 | 65 | // Server Info Command result 66 | private int currentCompanies; 67 | 68 | // Server Info Command result 69 | private int maxCompanies; 70 | 71 | // Server Info Command result 72 | private int currentSpectators; 73 | 74 | // holds the server process if the server is running 75 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 76 | private OpenttdProcess process; 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/OpenttdServerConfigGet.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @Data 9 | public class OpenttdServerConfigGet { 10 | private int autoSaveMinutes; 11 | private int numberOfAutoSaveFilesToKeep; 12 | private int numberOfManuallySaveFilesToKeep; 13 | List servers = new ArrayList<>(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/OpenttdServerConfigUpdate.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OpenttdServerConfigUpdate { 7 | private int autoSaveMinutes; 8 | private int numberOfAutoSaveFilesToKeep; 9 | private int numberOfManuallySaveFilesToKeep; 10 | 11 | private String password; 12 | 13 | private String oldPassword; 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/ServerFile.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | @Data 8 | @Accessors(chain = true) 9 | public class ServerFile { 10 | 11 | // This value represents an owner id of the given file 12 | private String ownerId; 13 | 14 | // Optional name representation of the file owner 15 | private String ownerName; 16 | 17 | private String path; 18 | 19 | private String name; 20 | 21 | private ServerFileType type; 22 | 23 | 24 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 25 | private boolean exists = true; 26 | 27 | 28 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 29 | private long created; 30 | 31 | 32 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 33 | private long lastModified; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/ServerFileType.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | public enum ServerFileType { 4 | SAVE_GAME, 5 | CONFIG; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/ServiceError.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ServiceError { 7 | ServiceErrorType type; 8 | String message; 9 | String stackTrace; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/external/ServiceErrorType.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.external; 2 | 3 | public enum ServiceErrorType { 4 | VALIDATION_EXCEPTION, 5 | RUNTIME_EXCEPTION, 6 | UNKNOWN 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/internal/InternalOpenttdServerConfig.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.internal; 2 | 3 | import de.litexo.model.external.OpenttdServer; 4 | import lombok.Data; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @Data 10 | public class InternalOpenttdServerConfig { 11 | private String path; 12 | private int autoSaveMinutes = 5; 13 | private int numberOfAutoSaveFilesToKeep = 10; 14 | private int numberOfManuallySaveFilesToKeep = 10; 15 | 16 | // sha256 hash of the admin password 17 | 18 | private String passwordSha256Hash; 19 | 20 | List servers = new ArrayList<>(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/mapper/OpenttdServerConfigMapper.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.mapper; 2 | 3 | import de.litexo.api.ServiceRuntimeException; 4 | import de.litexo.model.external.OpenttdServerConfigUpdate; 5 | import de.litexo.model.internal.InternalOpenttdServerConfig; 6 | import de.litexo.model.external.OpenttdServerConfigGet; 7 | import de.litexo.security.SecurityUtils; 8 | import org.mapstruct.AfterMapping; 9 | import org.mapstruct.BeanMapping; 10 | import org.mapstruct.Mapper; 11 | import org.mapstruct.Mapping; 12 | import org.mapstruct.MappingTarget; 13 | import org.mapstruct.NullValuePropertyMappingStrategy; 14 | import org.mapstruct.ReportingPolicy; 15 | 16 | @Mapper(config = ServiceMapperConfiguration.class, 17 | unmappedSourcePolicy = ReportingPolicy.WARN, 18 | unmappedTargetPolicy = ReportingPolicy.WARN) 19 | @SuppressWarnings("java:S1610") 20 | public abstract class OpenttdServerConfigMapper { 21 | public abstract OpenttdServerConfigGet toExternal(InternalOpenttdServerConfig internal); 22 | 23 | @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) 24 | @Mapping(target = "path", ignore = true) 25 | @Mapping(target = "passwordSha256Hash", ignore = true) 26 | @Mapping(target = "servers", ignore = true) 27 | // @formatter:on 28 | public abstract void patch(OpenttdServerConfigUpdate external, @MappingTarget InternalOpenttdServerConfig internal); 29 | 30 | @AfterMapping 31 | protected void patchAfterMapping(OpenttdServerConfigUpdate external, @MappingTarget InternalOpenttdServerConfig internal) { 32 | if (external.getPassword() != null && external.getOldPassword() != null) { 33 | if (SecurityUtils.isEquals(external.getOldPassword(), internal.getPasswordSha256Hash())) { 34 | internal.setPasswordSha256Hash(SecurityUtils.toSHA256(external.getPassword())); 35 | } else { 36 | throw new ServiceRuntimeException("Could not update password. Old password is not correct!"); 37 | } 38 | } 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/mapper/OpenttdServerMapper.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.mapper; 2 | 3 | import de.litexo.model.external.OpenttdServer; 4 | import org.mapstruct.BeanMapping; 5 | import org.mapstruct.Mapper; 6 | import org.mapstruct.Mapping; 7 | import org.mapstruct.MappingTarget; 8 | import org.mapstruct.NullValuePropertyMappingStrategy; 9 | import org.mapstruct.ReportingPolicy; 10 | 11 | @Mapper(config = ServiceMapperConfiguration.class, 12 | unmappedSourcePolicy = ReportingPolicy.WARN, 13 | unmappedTargetPolicy = ReportingPolicy.WARN) 14 | @SuppressWarnings("java:S1610") 15 | public abstract class OpenttdServerMapper { 16 | @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) 17 | @Mapping(target = "id", ignore = true) 18 | @Mapping(target = "process", ignore = true) 19 | // @formatter:on 20 | public abstract void patch(OpenttdServer external, @MappingTarget OpenttdServer internal); 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/model/mapper/ServiceMapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.litexo.model.mapper; 2 | 3 | import org.mapstruct.MapperConfig; 4 | import org.mapstruct.MappingInheritanceStrategy; 5 | 6 | @MapperConfig(componentModel = "cdi", 7 | mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG) 8 | public class ServiceMapperConfiguration { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/scheduler/Autosave.java: -------------------------------------------------------------------------------- 1 | package de.litexo.scheduler; 2 | 3 | 4 | import de.litexo.OpenttdProcess; 5 | import de.litexo.model.external.OpenttdServer; 6 | import de.litexo.model.internal.InternalOpenttdServerConfig; 7 | import de.litexo.services.OpenttdService; 8 | import io.quarkus.scheduler.Scheduled; 9 | 10 | import javax.enterprise.context.ApplicationScoped; 11 | import javax.inject.Inject; 12 | import java.util.Optional; 13 | 14 | @ApplicationScoped 15 | public class Autosave { 16 | 17 | @Inject 18 | OpenttdService service; 19 | 20 | @Scheduled(every = "10s") 21 | void checkAutosave() { 22 | InternalOpenttdServerConfig serverConfig = this.service.getOpenttdServerConfig(); 23 | if (serverConfig.getAutoSaveMinutes() <= 0) { 24 | System.out.println("Auto save is skipped because save intervall is <= 0 ->" + serverConfig.getAutoSaveMinutes()); 25 | return; 26 | } 27 | for (OpenttdProcess process : service.getProcesses()) { 28 | Optional openttdServer = service.getOpenttdServer(process.getId()); 29 | if (openttdServer.isPresent()) { 30 | if (openttdServer.get().getSaveGame() == null || !openttdServer.get().getSaveGame().isExists()) { 31 | save(openttdServer.get()); 32 | } else { 33 | long ageInSeconds = (System.currentTimeMillis() - openttdServer.get().getSaveGame().getLastModified()) / 1000; 34 | long outDated = serverConfig.getAutoSaveMinutes() * 60; 35 | if (ageInSeconds > outDated) { 36 | System.out.println("Last save game was made before '" + ageInSeconds + "' seconds. autosave is executed"); 37 | save(openttdServer.get()); 38 | } else { 39 | System.out.println("Last save game was made '" + ageInSeconds + "' no autosave was executed"); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | private void save(OpenttdServer server) { 47 | if (!server.isAutoSave()) { 48 | System.out.println("Autosave is disabled for Server: " + server.getName()); 49 | } else { 50 | this.service.autoSaveGame(server.getId()); 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/scheduler/Housekeeping.java: -------------------------------------------------------------------------------- 1 | package de.litexo.scheduler; 2 | 3 | 4 | import de.litexo.OpenttdProcess; 5 | import de.litexo.model.external.OpenttdServer; 6 | import de.litexo.model.external.ServerFile; 7 | import de.litexo.model.internal.InternalOpenttdServerConfig; 8 | import de.litexo.repository.DefaultRepository; 9 | import de.litexo.services.OpenttdService; 10 | import io.quarkus.scheduler.Scheduled; 11 | import org.apache.commons.io.FileUtils; 12 | 13 | import javax.enterprise.context.ApplicationScoped; 14 | import javax.inject.Inject; 15 | import java.io.File; 16 | import java.util.Comparator; 17 | import java.util.List; 18 | import java.util.Optional; 19 | import java.util.stream.Collectors; 20 | 21 | import static de.litexo.services.OpenttdService.AUTO_SAVE_INFIX; 22 | import static de.litexo.services.OpenttdService.MANUALLY_SAVE_INFIX; 23 | 24 | @ApplicationScoped 25 | public class Housekeeping { 26 | 27 | @Inject 28 | OpenttdService service; 29 | 30 | @Inject 31 | DefaultRepository repository; 32 | 33 | @Scheduled(every = "600s") 34 | void deleteOldAutosaves() { 35 | InternalOpenttdServerConfig serverConfig = this.service.getOpenttdServerConfig(); 36 | List openttdSaveGames = this.repository.getOpenttdSaveGames(); 37 | 38 | for (OpenttdProcess process : service.getProcesses()) { 39 | Optional serverOpt = service.getOpenttdServer(process.getId()); 40 | if (serverOpt.isPresent()) { 41 | OpenttdServer server = serverOpt.get(); 42 | 43 | // oldest begins with index 0 44 | List autoSaveGames = openttdSaveGames.stream() 45 | .filter(f -> f.getName().contains(server.getId()) && f.getName().contains(AUTO_SAVE_INFIX)) 46 | .sorted(Comparator.comparingLong(ServerFile::getLastModified)).toList(); 47 | 48 | // oldest begins with index 0 49 | List manSaveGames = openttdSaveGames.stream() 50 | .filter(f -> f.getName().contains(server.getId()) && f.getName().contains(MANUALLY_SAVE_INFIX)) 51 | .sorted(Comparator.comparingLong(ServerFile::getLastModified)).toList(); 52 | 53 | if (autoSaveGames.size() > serverConfig.getNumberOfAutoSaveFilesToKeep()) { 54 | List toDelete = autoSaveGames.subList(0,autoSaveGames.size()- serverConfig.getNumberOfAutoSaveFilesToKeep()); 55 | for (ServerFile f : toDelete) { 56 | FileUtils.deleteQuietly(new File(f.getPath())); 57 | } 58 | } 59 | 60 | if (manSaveGames.size() > serverConfig.getNumberOfManuallySaveFilesToKeep()) { 61 | List toDelete = manSaveGames.subList(0,manSaveGames.size()-serverConfig.getNumberOfManuallySaveFilesToKeep()); 62 | for (ServerFile f : toDelete) { 63 | FileUtils.deleteQuietly(new File(f.getPath())); 64 | } 65 | } 66 | 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/scheduler/UpdateServerInfo.java: -------------------------------------------------------------------------------- 1 | package de.litexo.scheduler; 2 | 3 | 4 | import de.litexo.OpenttdProcess; 5 | import de.litexo.commands.ServerInfoCommand; 6 | import de.litexo.events.EventBus; 7 | import de.litexo.events.OpenttdTerminalUpdateEvent; 8 | import de.litexo.model.external.OpenttdServer; 9 | import de.litexo.services.OpenttdService; 10 | import io.quarkus.scheduler.Scheduled; 11 | import org.eclipse.microprofile.context.ManagedExecutor; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.enterprise.context.ApplicationScoped; 15 | import javax.inject.Inject; 16 | import java.util.Optional; 17 | 18 | @ApplicationScoped 19 | public class UpdateServerInfo { 20 | 21 | @Inject 22 | OpenttdService service; 23 | 24 | @Inject 25 | EventBus eventBus; 26 | 27 | @Inject 28 | ManagedExecutor executor; 29 | 30 | 31 | @PostConstruct 32 | void init() { 33 | System.out.println("INIT UpdateServerInfo Scheduler"); 34 | this.eventBus.observe(OpenttdTerminalUpdateEvent.class, this.getClass(), openttdTerminalUpdateEvent -> 35 | // Important execute this async because the subscriber is blocking the emitter of the event 36 | executor.execute(() -> handleTerminalUpdateEvent(openttdTerminalUpdateEvent)) 37 | ); 38 | } 39 | 40 | @Scheduled(every = "120s") 41 | void checkAutoPauseUnpause() { 42 | for (OpenttdProcess process : service.getProcesses()) { 43 | updateServerInfo(process); 44 | } 45 | } 46 | 47 | void updateServerInfo(OpenttdProcess process) { 48 | OpenttdServer openttdServer = service.getOpenttdServer(process.getId()).orElse(null); 49 | if (openttdServer != null) { 50 | ServerInfoCommand cmd = process.executeCommand(new ServerInfoCommand(), false); 51 | if (cmd.isExecuted()) { 52 | openttdServer.setInviteCode(cmd.getInviteCode()); 53 | openttdServer.setCurrentClients(cmd.getCurrentClients()); 54 | openttdServer.setMaxClients(cmd.getMaxClients()); 55 | openttdServer.setCurrentCompanies(cmd.getCurrentCompanies()); 56 | openttdServer.setMaxCompanies(cmd.getMaxCompanies()); 57 | openttdServer.setCurrentSpectators(cmd.getCurrentSpectators()); 58 | this.service.updateServer(openttdServer.getId(), openttdServer); 59 | } 60 | } 61 | } 62 | 63 | void handleTerminalUpdateEvent(OpenttdTerminalUpdateEvent openttdTerminalUpdateEvent) { 64 | if ( 65 | openttdTerminalUpdateEvent.getText().contains("has started a new company") 66 | || openttdTerminalUpdateEvent.getText().contains("has joined company") 67 | || openttdTerminalUpdateEvent.getText().contains("has joined the game") 68 | || openttdTerminalUpdateEvent.getText().contains("has left the game") 69 | || openttdTerminalUpdateEvent.getText().contains("closed connection") 70 | || openttdTerminalUpdateEvent.getText().contains("has joined spectators") 71 | ) { 72 | Optional process = this.service.getProcesses().stream().filter(p -> p.getProcessThread().getUuid().equals(openttdTerminalUpdateEvent.getProcessId())).findAny(); 73 | if (process.isPresent()) { 74 | updateServerInfo(process.get()); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/security/AuthResource.java: -------------------------------------------------------------------------------- 1 | package de.litexo.security; 2 | 3 | import javax.annotation.security.PermitAll; 4 | import javax.inject.Inject; 5 | import javax.ws.rs.HeaderParam; 6 | import javax.ws.rs.POST; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.core.HttpHeaders; 9 | import javax.ws.rs.core.Response; 10 | import java.util.Optional; 11 | 12 | import static de.litexo.security.SecurityService.HEADER_OPENTTD_SERVER_SESSION_ID; 13 | 14 | 15 | @Path("/api/auth") 16 | @PermitAll 17 | public class AuthResource { 18 | 19 | @Inject 20 | SecurityService securityService; 21 | 22 | @Path("/login") 23 | @POST() 24 | public Response login(@HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) { 25 | Optional login = securityService.login(authHeader); 26 | if (login.isPresent()) { 27 | return Response.status(200).header(HEADER_OPENTTD_SERVER_SESSION_ID, login.get().getSessionId()).build(); 28 | } 29 | return Response.status(401).build(); 30 | } 31 | 32 | @Path("/verifyLogin") 33 | @POST() 34 | public Response verifyLogin(@HeaderParam(HEADER_OPENTTD_SERVER_SESSION_ID) String session) { 35 | if (securityService.isLoggedIn(session)) { 36 | return Response.status(200).header(HEADER_OPENTTD_SERVER_SESSION_ID, session).build(); 37 | } 38 | return Response.status(401).build(); 39 | } 40 | 41 | @Path("/logout") 42 | @POST 43 | public Response logout(@HeaderParam(HEADER_OPENTTD_SERVER_SESSION_ID) String session) { 44 | securityService.logout(session); 45 | return Response.ok().build(); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/security/BasicAuth.java: -------------------------------------------------------------------------------- 1 | package de.litexo.security; 2 | 3 | import de.litexo.api.ServiceRuntimeException; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Base64; 7 | 8 | public class BasicAuth { 9 | 10 | private String password; 11 | private String userName; 12 | 13 | public BasicAuth(String authHeader) { 14 | 15 | try { 16 | // Authorization: Basic base64credentials 17 | String base64Credentials = authHeader.substring("Basic".length()).trim(); 18 | byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); 19 | String credentials = new String(credDecoded, StandardCharsets.UTF_8); 20 | // credentials = username:password 21 | final String[] values = credentials.split(":", 2); 22 | if (values.length == 2) { 23 | this.userName = values[0].trim(); 24 | this.password = values[1].trim(); 25 | } 26 | } catch (IllegalArgumentException e) { 27 | throw new ServiceRuntimeException(e); 28 | } 29 | 30 | } 31 | 32 | /** 33 | * Gets the value of the password property. 34 | * 35 | * @return possible object is {@link String} 36 | */ 37 | public String getPassword() { 38 | 39 | return password; 40 | } 41 | 42 | /** 43 | * Gets the value of the userName property. 44 | * 45 | * @return possible object is {@link String} 46 | */ 47 | public String getUserName() { 48 | 49 | return userName; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/security/BasicAuthSession.java: -------------------------------------------------------------------------------- 1 | package de.litexo.security; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.ws.rs.core.SecurityContext; 7 | import java.util.UUID; 8 | 9 | 10 | public class BasicAuthSession { 11 | @Getter 12 | private String sessionId = UUID.randomUUID().toString(); 13 | 14 | @Getter 15 | @Setter 16 | private long lastUpdate; 17 | 18 | @Getter 19 | @Setter 20 | private String user; 21 | 22 | @Getter 23 | @Setter 24 | private SecurityContext securityContext; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/security/SecurityFiler.java: -------------------------------------------------------------------------------- 1 | package de.litexo.security; 2 | 3 | import javax.inject.Inject; 4 | import javax.ws.rs.container.ContainerRequestContext; 5 | import javax.ws.rs.container.ContainerRequestFilter; 6 | import javax.ws.rs.container.PreMatching; 7 | import javax.ws.rs.ext.Provider; 8 | import java.io.IOException; 9 | 10 | @Provider 11 | @PreMatching 12 | public class SecurityFiler implements ContainerRequestFilter { 13 | @Inject 14 | SecurityService securityService; 15 | 16 | 17 | @Override 18 | public void filter(ContainerRequestContext requestContext) throws IOException { 19 | securityService.validatedLoginSession(requestContext); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/security/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package de.litexo.security; 2 | 3 | import org.apache.commons.codec.digest.DigestUtils; 4 | import org.apache.commons.lang3.RandomStringUtils; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | public class SecurityUtils { 11 | 12 | private SecurityUtils() { 13 | throw new IllegalStateException("Utility class"); 14 | } 15 | 16 | public static String toSHA256(String value) { 17 | return DigestUtils.sha256Hex(value); 18 | } 19 | 20 | public static boolean isEquals(String value, String sha256Value) { 21 | return toSHA256(value).equalsIgnoreCase(sha256Value); 22 | } 23 | 24 | public static String generatePassword() { 25 | 26 | String upperCaseLetters = RandomStringUtils.random(2, 65, 90, true, true); 27 | String lowerCaseLetters = RandomStringUtils.random(2, 97, 122, true, true); 28 | String numbers = RandomStringUtils.randomNumeric(2); 29 | String specialChar = RandomStringUtils.random(2, 33, 47, false, false); 30 | String totalChars = RandomStringUtils.randomAlphanumeric(2); 31 | String combinedChars = upperCaseLetters.concat(lowerCaseLetters).concat(numbers).concat(specialChar).concat(totalChars); 32 | List pwdChars = combinedChars.chars().mapToObj(c -> (char) c).collect(Collectors.toList()); 33 | Collections.shuffle(pwdChars); 34 | String password = pwdChars.stream().collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString(); 35 | return password; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/services/ApplicationBootstrapService.java: -------------------------------------------------------------------------------- 1 | package de.litexo.services; 2 | 3 | import de.litexo.OpenttdProcess; 4 | import de.litexo.model.internal.InternalOpenttdServerConfig; 5 | import de.litexo.repository.DefaultRepository; 6 | import de.litexo.security.SecurityUtils; 7 | import io.quarkus.runtime.ShutdownEvent; 8 | import io.quarkus.runtime.Startup; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.eclipse.microprofile.config.inject.ConfigProperty; 11 | import org.jboss.logging.Logger; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.enterprise.context.ApplicationScoped; 15 | import javax.enterprise.event.Observes; 16 | import javax.inject.Inject; 17 | import java.util.Optional; 18 | 19 | @Startup 20 | @ApplicationScoped 21 | public class ApplicationBootstrapService { 22 | @ConfigProperty(name = "server.initial.password") 23 | Optional initialPassword; 24 | 25 | private static final Logger LOG = Logger.getLogger(ApplicationBootstrapService.class); 26 | 27 | @Inject 28 | DefaultRepository repository; 29 | 30 | @Inject 31 | OpenttdService service; 32 | 33 | @PostConstruct 34 | void init() { 35 | initDefaultServerConfig(); 36 | 37 | } 38 | 39 | private void initDefaultServerConfig() { 40 | InternalOpenttdServerConfig openttdServerConfig = this.repository.getOpenttdServerConfig(); 41 | if (StringUtils.isBlank(openttdServerConfig.getPasswordSha256Hash())) { 42 | System.out.println(); 43 | System.out.println("###########################################################################"); 44 | if (this.initialPassword.isPresent()) { 45 | System.out.println(String.format("### Initial password was set as environment variable '%s' and will be the password\n### for the 'admin' user.", "server.initial.password")); 46 | openttdServerConfig.setPasswordSha256Hash(SecurityUtils.toSHA256(this.initialPassword.get())); 47 | } else { 48 | String password = SecurityUtils.generatePassword(); 49 | System.out.println(String.format("### No initial password was set. A password for 'admin' will be generated.\n### Copy it NOW, because it will never be shown again.\n### Password: %s", password)); 50 | openttdServerConfig.setPasswordSha256Hash(SecurityUtils.toSHA256(password)); 51 | } 52 | this.repository.save(openttdServerConfig); 53 | System.out.println("###########################################################################"); 54 | System.out.println(); 55 | } 56 | } 57 | 58 | void onStop(@Observes ShutdownEvent ev) { 59 | System.out.println("The application is stopping. Will terminate all open processes!"); 60 | for (OpenttdProcess p : service.getProcesses()) { 61 | try { 62 | p.getProcessThread().stop(); 63 | } catch (Exception e) { 64 | e.printStackTrace(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/de/litexo/websocket/DataStreamWebSocket.java: -------------------------------------------------------------------------------- 1 | package de.litexo.websocket; 2 | 3 | import de.litexo.events.OpenttdTerminalUpdateEvent; 4 | import de.litexo.events.EventBus; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.enterprise.context.ApplicationScoped; 8 | import javax.inject.Inject; 9 | import javax.websocket.OnClose; 10 | import javax.websocket.OnError; 11 | import javax.websocket.OnMessage; 12 | import javax.websocket.OnOpen; 13 | import javax.websocket.Session; 14 | import javax.websocket.server.PathParam; 15 | import javax.websocket.server.ServerEndpoint; 16 | import java.util.Map; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | @ServerEndpoint("/data-stream") 20 | @ApplicationScoped 21 | public class DataStreamWebSocket { 22 | 23 | Map sessions = new ConcurrentHashMap<>(); 24 | 25 | 26 | @Inject 27 | EventBus eventBus; 28 | 29 | @PostConstruct 30 | void init() { 31 | this.eventBus.observe(OpenttdTerminalUpdateEvent.class, this, e -> this.broadcast(e.toJson())); 32 | } 33 | 34 | @OnOpen 35 | public void onOpen(Session session) { 36 | sessions.put(session.getId(), session); 37 | } 38 | 39 | @OnClose 40 | public void onClose(Session session) { 41 | sessions.remove(session.getId()); 42 | 43 | } 44 | 45 | @OnError 46 | public void onError(Session session, Throwable throwable) { 47 | sessions.remove(session.getId()); 48 | 49 | } 50 | 51 | @OnMessage 52 | public void onMessage(String message, @PathParam("username") String username) { 53 | 54 | } 55 | 56 | private void broadcast(String message) { 57 | sessions.values().forEach(s -> s.getAsyncRemote().sendObject(message, result -> { 58 | if (result.getException() != null) { 59 | System.out.println("Unable to send message: " + result.getException()); 60 | } 61 | })); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.http.cors=true 2 | quarkus.http.cors.exposed-headers=X-OPENTTD_SERVER_SESSION_ID 3 | 4 | openttd.save.dir=/tmp/openttd/save 5 | openttd.config.dir=/tmp/openttd/config 6 | server.config.dir=/tmp/openttd 7 | 8 | quarkus.package.type=mutable-jar 9 | quarkus.live-reload.password=Password_1 10 | quarkus.live-reload.url=http://localhost:8080 11 | 12 | 13 | %dev.start-server.command=cmd.exe; /C; C:\\Development\\Git\\openttd-server\\loop-input.bat 14 | %dev.save.dir=C:\\Temp\\openttd\save 15 | 16 | 17 | # linux development 18 | %dev.start-server.command=openttd; -D; 19 | %dev.openttd.save.dir=/home/andreas/.local/share/openttd/save 20 | %dev.openttd.config.dir=/home/andreas/.config/openttd 21 | %dev.server.config.dir=/tmp/oppenttd-server-config 22 | 23 | # This should be set as env variable 24 | %dev.server.initial.password=Password_1 25 | 26 | %dev.quarkus.http.cors=true 27 | #%dev.server.disable.security=true 28 | -------------------------------------------------------------------------------- /src/main/resources/templates/openttd-configs/private.cfg: -------------------------------------------------------------------------------- 1 | ; This file possibly contains private information which can identify you as person. 2 | [private] 3 | 4 | [network] 5 | client_name = server_admin 6 | server_name = unknown server 7 | connect_to_ip = 8 | last_joined = 9 | 10 | [server_bind_addresses] 11 | 12 | [servers] 13 | 14 | [bans] 15 | 16 | [version] 17 | version_string = 12.2 18 | version_number = 1C286D64 19 | ini_version = 2 20 | -------------------------------------------------------------------------------- /src/main/resources/templates/openttd-configs/secrets.cfg: -------------------------------------------------------------------------------- 1 | ; Do not share this file with others, not even if they claim to be technical support. 2 | ; This file contains saved passwords and other secrets that should remain private to you! 3 | [secrets] 4 | 5 | [network] 6 | server_password = 7 | rcon_password = 8 | admin_password = 9 | default_company_pass = 10 | network_id = 11 | server_invite_code = 12 | server_invite_code_secret = 13 | 14 | [version] 15 | version_string = 12.2 16 | version_number = 1C286D64 17 | ini_version = 2 18 | -------------------------------------------------------------------------------- /src/test/java/de/litexo/commands/ClientsCommandTest.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.ProcessThread; 4 | import org.apache.commons.io.IOUtils; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.UUID; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class ClientsCommandTest { 22 | 23 | @Mock 24 | ProcessThread process; 25 | 26 | ClientsCommand subject = new ClientsCommand(); 27 | 28 | @BeforeEach 29 | void beforeEach() { 30 | this.subject.marker = "@@@-xxx-asdasd"; 31 | } 32 | 33 | @Test 34 | void test001() throws Exception { 35 | String logs = IOUtils.resourceToString("/command-samples/clients.txt", StandardCharsets.UTF_8); 36 | when(this.process.getLogs()).thenReturn(logs); 37 | 38 | ClientsCommand execute = (ClientsCommand) this.subject.execute(process, UUID.randomUUID().toString()); 39 | 40 | assertTrue(execute.isExecuted()); 41 | assertEquals(3,execute.getClients().size()); 42 | 43 | assertEquals("1",execute.getClients().get(0).getIndex()); 44 | assertEquals("Andreas Hauschild'",execute.getClients().get(0).getName()); 45 | assertEquals("255",execute.getClients().get(0).getCompany()); 46 | assertEquals("server",execute.getClients().get(0).getIp()); 47 | 48 | assertEquals("2",execute.getClients().get(1).getIndex()); 49 | assertEquals("Unnamed Client",execute.getClients().get(1).getName()); 50 | assertEquals("255",execute.getClients().get(1).getCompany()); 51 | assertEquals("10.0.2.15",execute.getClients().get(1).getIp()); 52 | 53 | assertEquals("4",execute.getClients().get(2).getIndex()); 54 | assertEquals("Unnamed Client #1",execute.getClients().get(2).getName()); 55 | assertEquals("255",execute.getClients().get(2).getCompany()); 56 | assertEquals("10.0.2.15",execute.getClients().get(2).getIp()); 57 | 58 | System.out.println(execute.getClients()); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/de/litexo/commands/PauseCommandTest.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.ProcessThread; 4 | import de.litexo.repository.DefaultRepository; 5 | import org.apache.commons.io.IOUtils; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.UUID; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | import static org.mockito.Mockito.when; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | class PauseCommandTest { 21 | 22 | @Mock 23 | ProcessThread process; 24 | 25 | @Mock 26 | DefaultRepository repository; 27 | 28 | 29 | PauseCommand subject; 30 | 31 | @BeforeEach 32 | void beforeEach() { 33 | subject = new PauseCommand(repository); 34 | this.subject.marker = "@@@-xxx-asdasd"; 35 | } 36 | 37 | @Test 38 | void test001() throws Exception { 39 | String logs = IOUtils.resourceToString("/command-samples/pause.txt", StandardCharsets.UTF_8); 40 | when(this.process.getLogs()).thenReturn(logs); 41 | 42 | this.subject.execute(process, UUID.randomUUID().toString()); 43 | 44 | assertTrue(this.subject.isExecuted()); 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/java/de/litexo/commands/ServerInfoCommandTest.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.ProcessThread; 4 | import org.apache.commons.io.IOUtils; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.UUID; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | import static org.mockito.Mockito.when; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class ServerInfoCommandTest { 20 | 21 | @Mock 22 | ProcessThread process; 23 | 24 | ServerInfoCommand subject = new ServerInfoCommand(); 25 | 26 | @BeforeEach 27 | void beforeEach() { 28 | this.subject.marker = "@@@-xxx-asdasd"; 29 | } 30 | 31 | @Test 32 | void test001() throws Exception { 33 | String logs = IOUtils.resourceToString("/command-samples/serverInfo.txt", StandardCharsets.UTF_8); 34 | when(this.process.getLogs()).thenReturn(logs); 35 | 36 | ServerInfoCommand execute = (ServerInfoCommand) this.subject.execute(process, UUID.randomUUID().toString()); 37 | 38 | assertTrue(execute.isExecuted()); 39 | assertEquals("asdasd",execute.getInviteCode()); 40 | assertEquals(3,execute.getCurrentClients()); 41 | assertEquals(25,execute.getMaxClients()); 42 | assertEquals(5,execute.getCurrentCompanies()); 43 | assertEquals(15,execute.getMaxCompanies()); 44 | assertEquals(1,execute.getCurrentSpectators()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/de/litexo/commands/UnpauseCommandTest.java: -------------------------------------------------------------------------------- 1 | package de.litexo.commands; 2 | 3 | import de.litexo.ProcessThread; 4 | import de.litexo.repository.DefaultRepository; 5 | import org.apache.commons.io.IOUtils; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.UUID; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | import static org.mockito.Mockito.when; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class UnpauseCommandTest { 20 | 21 | @Mock 22 | ProcessThread process; 23 | 24 | @Mock() 25 | DefaultRepository repository; 26 | 27 | UnpauseCommand subject; 28 | 29 | @BeforeEach 30 | void beforeEach() { 31 | subject = new UnpauseCommand(repository); 32 | this.subject.marker = "@@@-xxx-asdasd"; 33 | } 34 | 35 | @Test 36 | void test001() throws Exception { 37 | String logs = IOUtils.resourceToString("/command-samples/unpause.txt", StandardCharsets.UTF_8); 38 | when(this.process.getLogs()).thenReturn(logs); 39 | 40 | this.subject.execute(process, UUID.randomUUID().toString()); 41 | 42 | assertTrue(this.subject.isExecuted()); 43 | } 44 | } -------------------------------------------------------------------------------- /src/test/resources/command-samples/clients.txt: -------------------------------------------------------------------------------- 1 | dbg: [net] Starting dedicated server, version 12.2 2 | dbg: [net] Starting network 3 | dbg: [net] Initializing UDP listeners 4 | dbg: [net] Network online, multiplayer available 5 | dbg: [net] Detected broadcast addresses: 6 | dbg: [net] 0) 10.0.2.255 7 | OpenTTD Game Console Revision 7 - 12.2 8 | ------------------------------------ 9 | use "help" for more information. 10 | 11 | dbg: [net] No "server_name" has been set, using "Unnamed Server" instead. Please set this now using the "server_name " command 12 | dbg: [net] Initializing UDP listeners 13 | dbg: [net] Initializing UDP listeners 14 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 15 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 16 | dbg: [net] Network revision name: 12.2 17 | dbg: [net] Generating map, please wait... 18 | dbg: [net] Map generation percentage complete: 5 19 | dbg: [net] Map generation percentage complete: 10 20 | dbg: [net] Map generation percentage complete: 15 21 | dbg: [net] Map generation percentage complete: 20 22 | dbg: [net] Map generation percentage complete: 25 23 | dbg: [net] Map generation percentage complete: 30 24 | dbg: [net] Map generation percentage complete: 35 25 | dbg: [net] Map generation percentage complete: 40 26 | dbg: [net] Map generation percentage complete: 45 27 | dbg: [net] Map generation percentage complete: 50 28 | dbg: [net] Map generation percentage complete: 55 29 | dbg: [net] Map generation percentage complete: 60 30 | dbg: [net] Map generation percentage complete: 65 31 | dbg: [net] Map generation percentage complete: 70 32 | dbg: [net] Map generation percentage complete: 75 33 | dbg: [net] Map generation percentage complete: 80 34 | dbg: [net] Map generation percentage complete: 85 35 | dbg: [net] Map generation percentage complete: 90 36 | dbg: [net] Map generation percentage complete: 99 37 | dbg: [net] Map generated, starting game 38 | save "/home/andreas/.local/share/openttd/save/Test_auto_save" 39 | 40 | Saving map... 41 | Map successfully saved to '/home/andreas/.local/share/openttd/save/Test_auto_save.sav'. 42 | cliets 43 | 44 | Command 'cliets' not found. 45 | clients 46 | 47 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 48 |  49 | 50 | Command '?[A' not found. 51 | clients 52 | 53 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 54 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 1968 55 | ‎*** Game paused (connecting clients) 56 | dbg: [net] [server] Client #2 (10.0.2.15) joined as Unnamed Client 57 | ‎*** Unnamed Client has joined the game (Client #2) 58 | ‎*** Game unpaused (connecting clients) 59 | clients 60 | 61 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 62 | Client #2 name: 'Unnamed Client' company: 255 IP: 10.0.2.15 63 | clients 64 | 65 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 66 | Client #2 name: 'Unnamed Client' company: 255 IP: 10.0.2.15 67 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 4960 68 | dbg: [net] [server] Client #3 closed connection 69 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 5016 70 | ‎*** Game paused (connecting clients) 71 | ‎*** Unnamed Client #1 has joined the game (Client #4) 72 | dbg: [net] [server] Client #4 (10.0.2.15) joined as Unnamed Client #1 73 | ‎*** Game unpaused (connecting clients) 74 | @@@-xxx-asdasd 75 | 76 | Command '@@@-xxx-asdasd' not found. 77 | clients 78 | 79 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 80 | Client #2 name: 'Unnamed Client' company: 255 IP: 10.0.2.15 81 | Client #4 name: 'Unnamed Client #1' company: 255 IP: 10.0.2.15 82 | -------------------------------------------------------------------------------- /src/test/resources/command-samples/pause.txt: -------------------------------------------------------------------------------- 1 | dbg: [net] Starting dedicated server, version 12.2 2 | dbg: [net] Starting network 3 | dbg: [net] Initializing UDP listeners 4 | dbg: [net] Network online, multiplayer available 5 | dbg: [net] Detected broadcast addresses: 6 | dbg: [net] 0) 10.0.2.255 7 | OpenTTD Game Console Revision 7 - 12.2 8 | ------------------------------------ 9 | use "help" for more information. 10 | 11 | dbg: [net] No "client_name" has been set, using "Unnamed Client" instead. Please set this now using the "name " command 12 | dbg: [net] No "server_name" has been set, using "Unnamed Server" instead. Please set this now using the "server_name " command 13 | dbg: [net] Initializing UDP listeners 14 | dbg: [net] Initializing UDP listeners 15 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 16 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 17 | dbg: [net] Network revision name: 12.2 18 | dbg: [net] Generating map, please wait... 19 | dbg: [net] Map generation percentage complete: 5 20 | dbg: [net] Map generation percentage complete: 10 21 | dbg: [net] Map generation percentage complete: 15 22 | dbg: [net] Map generation percentage complete: 20 23 | dbg: [net] Map generation percentage complete: 25 24 | dbg: [net] Map generation percentage complete: 30 25 | dbg: [net] Map generation percentage complete: 35 26 | dbg: [net] Map generation percentage complete: 40 27 | dbg: [net] Map generation percentage complete: 45 28 | dbg: [net] Map generation percentage complete: 50 29 | dbg: [net] Map generation percentage complete: 55 30 | dbg: [net] Map generation percentage complete: 60 31 | dbg: [net] Map generation percentage complete: 65 32 | dbg: [net] Map generation percentage complete: 70 33 | dbg: [net] Map generation percentage complete: 75 34 | dbg: [net] Map generation percentage complete: 80 35 | dbg: [net] Map generation percentage complete: 85 36 | dbg: [net] Map generation percentage complete: 90 37 | dbg: [net] Map generation percentage complete: 99 38 | dbg: [net] Map generated, starting game 39 | save "/home/andreas/.local/share/openttd/save/test_auto_save" 40 | 41 | Saving map... 42 | Map successfully saved to '/home/andreas/.local/share/openttd/save/test_auto_save.sav'. 43 | save "/home/andreas/.local/share/openttd/save/test_manually_save" 44 | 45 | Saving map... 46 | Map successfully saved to '/home/andreas/.local/share/openttd/save/test_manually_save.sav'. 47 | @@@-xxx-asdasd 48 | 49 | Command '@@@-xxx-asdasd' not found. 50 | pause 51 | 52 | ‎*** Game paused (manual) 53 | -------------------------------------------------------------------------------- /src/test/resources/command-samples/serverInfo.txt: -------------------------------------------------------------------------------- 1 | dbg: [net] Starting dedicated server, version 12.2 2 | dbg: [net] Starting network 3 | dbg: [net] Initializing UDP listeners 4 | dbg: [net] Network online, multiplayer available 5 | dbg: [net] Detected broadcast addresses: 6 | dbg: [net] 0) 10.0.2.255 7 | OpenTTD Game Console Revision 7 - 12.2 8 | ------------------------------------ 9 | use "help" for more information. 10 | 11 | dbg: [net] No "server_name" has been set, using "Unnamed Server" instead. Please set this now using the "server_name " command 12 | dbg: [net] Initializing UDP listeners 13 | dbg: [net] Initializing UDP listeners 14 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 15 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 16 | dbg: [net] Network revision name: 12.2 17 | dbg: [net] Generating map, please wait... 18 | dbg: [net] Map generation percentage complete: 5 19 | dbg: [net] Map generation percentage complete: 10 20 | dbg: [net] Map generation percentage complete: 15 21 | dbg: [net] Map generation percentage complete: 20 22 | dbg: [net] Map generation percentage complete: 25 23 | dbg: [net] Map generation percentage complete: 30 24 | dbg: [net] Map generation percentage complete: 35 25 | dbg: [net] Map generation percentage complete: 40 26 | dbg: [net] Map generation percentage complete: 45 27 | dbg: [net] Map generation percentage complete: 50 28 | dbg: [net] Map generation percentage complete: 55 29 | dbg: [net] Map generation percentage complete: 60 30 | dbg: [net] Map generation percentage complete: 65 31 | dbg: [net] Map generation percentage complete: 70 32 | dbg: [net] Map generation percentage complete: 75 33 | dbg: [net] Map generation percentage complete: 80 34 | dbg: [net] Map generation percentage complete: 85 35 | dbg: [net] Map generation percentage complete: 90 36 | dbg: [net] Map generation percentage complete: 99 37 | dbg: [net] Map generated, starting game 38 | save "/home/andreas/.local/share/openttd/save/Test_auto_save" 39 | 40 | Saving map... 41 | Map successfully saved to '/home/andreas/.local/share/openttd/save/Test_auto_save.sav'. 42 | cliets 43 | 44 | Command 'cliets' not found. 45 | clients 46 | 47 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 48 |  49 | 50 | Command '?[A' not found. 51 | clients 52 | 53 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 54 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 1968 55 | ‎*** Game paused (connecting clients) 56 | dbg: [net] [server] Client #2 (10.0.2.15) joined as Unnamed Client 57 | ‎*** Unnamed Client has joined the game (Client #2) 58 | ‎*** Game unpaused (connecting clients) 59 | clients 60 | 61 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 62 | Client #2 name: 'Unnamed Client' company: 255 IP: 10.0.2.15 63 | clients 64 | 65 | Client #1 name: 'Andreas Hauschild'' company: 255 IP: server 66 | Client #2 name: 'Unnamed Client' company: 255 IP: 10.0.2.15 67 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 4960 68 | dbg: [net] [server] Client #3 closed connection 69 | dbg: [net] [server] Client connected from 10.0.2.15 on frame 5016 70 | ‎*** Game paused (connecting clients) 71 | ‎*** Unnamed Client #1 has joined the game (Client #4) 72 | dbg: [net] [server] Client #4 (10.0.2.15) joined as Unnamed Client #1 73 | ‎*** Game unpaused (connecting clients) 74 | @@@-xxx-asdasd 75 | 76 | Command '@@@-xxx-asdasd' not found. 77 | server_info 78 | Invite code: asdasd 79 | Current/maximum clients: 3/ 25 80 | Current/maximum companies: 5/ 15 81 | Current spectators: 1 82 | -------------------------------------------------------------------------------- /src/test/resources/command-samples/unpause.txt: -------------------------------------------------------------------------------- 1 | dbg: [net] Starting dedicated server, version 12.2 2 | dbg: [net] Starting network 3 | dbg: [net] Initializing UDP listeners 4 | dbg: [net] Network online, multiplayer available 5 | dbg: [net] Detected broadcast addresses: 6 | dbg: [net] 0) 10.0.2.255 7 | OpenTTD Game Console Revision 7 - 12.2 8 | ------------------------------------ 9 | use "help" for more information. 10 | 11 | dbg: [net] No "client_name" has been set, using "Unnamed Client" instead. Please set this now using the "name " command 12 | dbg: [net] No "server_name" has been set, using "Unnamed Server" instead. Please set this now using the "server_name " command 13 | dbg: [net] Initializing UDP listeners 14 | dbg: [net] Initializing UDP listeners 15 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 16 | dbg: [net] Listening on 0.0.0.0:3979 (IPv4) 17 | dbg: [net] Network revision name: 12.2 18 | dbg: [net] Generating map, please wait... 19 | dbg: [net] Map generation percentage complete: 5 20 | dbg: [net] Map generation percentage complete: 10 21 | dbg: [net] Map generation percentage complete: 15 22 | dbg: [net] Map generation percentage complete: 20 23 | dbg: [net] Map generation percentage complete: 25 24 | dbg: [net] Map generation percentage complete: 30 25 | dbg: [net] Map generation percentage complete: 35 26 | dbg: [net] Map generation percentage complete: 40 27 | dbg: [net] Map generation percentage complete: 45 28 | dbg: [net] Map generation percentage complete: 50 29 | dbg: [net] Map generation percentage complete: 55 30 | dbg: [net] Map generation percentage complete: 60 31 | dbg: [net] Map generation percentage complete: 65 32 | dbg: [net] Map generation percentage complete: 70 33 | dbg: [net] Map generation percentage complete: 75 34 | dbg: [net] Map generation percentage complete: 80 35 | dbg: [net] Map generation percentage complete: 85 36 | dbg: [net] Map generation percentage complete: 90 37 | dbg: [net] Map generation percentage complete: 99 38 | dbg: [net] Map generated, starting game 39 | save "/home/andreas/.local/share/openttd/save/test_auto_save" 40 | 41 | Saving map... 42 | Map successfully saved to '/home/andreas/.local/share/openttd/save/test_auto_save.sav'. 43 | save "/home/andreas/.local/share/openttd/save/test_manually_save" 44 | 45 | Saving map... 46 | Map successfully saved to '/home/andreas/.local/share/openttd/save/test_manually_save.sav'. 47 | @@@-xxx-asdasd 48 | 49 | Command '@@@-xxx-asdasd' not found. 50 | pause 51 | 52 | ‎*** Game unpaused (manual) 53 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /ui/.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 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | /node 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # TradingBotUi 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /ui/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/openttd-server-ui'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /ui/output: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/output -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openttd-server-ui", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "localBuild": "node node_modules/@angular/cli/bin/ng build --output-hashing=all" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@andreashauschild/just-upload": "^1.1.0", 15 | "@angular/animations": "^14.2.0", 16 | "@angular/cdk": "^14.2.2", 17 | "@angular/common": "^14.2.0", 18 | "@angular/compiler": "^14.2.0", 19 | "@angular/core": "^14.2.0", 20 | "@angular/forms": "^14.2.0", 21 | "@angular/material": "^14.2.2", 22 | "@angular/platform-browser": "^14.2.0", 23 | "@angular/platform-browser-dynamic": "^14.2.0", 24 | "@angular/router": "^14.2.0", 25 | "@ngrx/effects": "^14.3.2", 26 | "@ngrx/store": "^14.3.2", 27 | "@ngrx/store-devtools": "^14.3.2", 28 | "@ngx-loading-bar/core": "^6.0.2", 29 | "@ngx-loading-bar/http-client": "^6.0.2", 30 | "@tailwindcss/forms": "^0.5.3", 31 | "angular-google-charts": "^2.2.3", 32 | "highcharts": "^10.2.1", 33 | "highcharts-angular": "^3.0.0", 34 | "ng-openapi-gen": "^0.23.0", 35 | "ng-terminal": "^5.2.0", 36 | "reconnecting-websocket": "^4.4.0", 37 | "rxjs": "~7.5.0", 38 | "tslib": "^2.3.0", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "^14.2.3", 43 | "@angular/cli": "~14.2.3", 44 | "@angular/compiler-cli": "^14.2.0", 45 | "@ngrx/schematics": "^14.3.2", 46 | "@types/jasmine": "~4.0.0", 47 | "autoprefixer": "^10.4.11", 48 | "jasmine-core": "~4.3.0", 49 | "karma": "~6.4.0", 50 | "karma-chrome-launcher": "~3.1.0", 51 | "karma-coverage": "~2.2.0", 52 | "karma-jasmine": "~5.1.0", 53 | "karma-jasmine-html-reporter": "~2.0.0", 54 | "postcss": "^8.4.16", 55 | "tailwindcss": "^3.1.8", 56 | "typescript": "~4.7.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": { 3 | "target": "http://localhost:8080", 4 | "secure": true, 5 | "logLevel": "debug", 6 | "changeOrigin": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/app/api/api-configuration.ts: -------------------------------------------------------------------------------- 1 | import {environment} from '../../environments/environment'; 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import { Injectable } from '@angular/core'; 5 | 6 | /** 7 | * Global configuration 8 | */ 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ApiConfiguration { 13 | rootUrl: string = environment.baseUrl; 14 | } 15 | 16 | /** 17 | * Parameters for `ApiModule.forRoot()` 18 | */ 19 | export interface ApiConfigurationParams { 20 | rootUrl?: string; 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/api/api.module.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { NgModule, ModuleWithProviders, SkipSelf, Optional } from '@angular/core'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { ApiConfiguration, ApiConfigurationParams } from './api-configuration'; 6 | 7 | import { AuthResourceService } from './services/auth-resource.service'; 8 | import { ChunkUploadResourceService } from './services/chunk-upload-resource.service'; 9 | import { OpenttdServerResourceService } from './services/openttd-server-resource.service'; 10 | 11 | /** 12 | * Module that provides all services and configuration. 13 | */ 14 | @NgModule({ 15 | imports: [], 16 | exports: [], 17 | declarations: [], 18 | providers: [ 19 | AuthResourceService, 20 | ChunkUploadResourceService, 21 | OpenttdServerResourceService, 22 | ApiConfiguration 23 | ], 24 | }) 25 | export class ApiModule { 26 | static forRoot(params: ApiConfigurationParams): ModuleWithProviders { 27 | return { 28 | ngModule: ApiModule, 29 | providers: [ 30 | { 31 | provide: ApiConfiguration, 32 | useValue: params 33 | } 34 | ] 35 | } 36 | } 37 | 38 | constructor( 39 | @Optional() @SkipSelf() parentModule: ApiModule, 40 | @Optional() http: HttpClient 41 | ) { 42 | if (parentModule) { 43 | throw new Error('ApiModule is already loaded. Import in your base AppModule only.'); 44 | } 45 | if (!http) { 46 | throw new Error('You need to import the HttpClientModule in your AppModule! \n' + 47 | 'See also https://github.com/angular/angular/issues/20575'); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/api/base-service.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { Injectable } from '@angular/core'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { ApiConfiguration } from './api-configuration'; 6 | 7 | /** 8 | * Base class for services 9 | */ 10 | @Injectable() 11 | export class BaseService { 12 | constructor( 13 | protected config: ApiConfiguration, 14 | protected http: HttpClient 15 | ) { 16 | } 17 | 18 | private _rootUrl: string = ''; 19 | 20 | /** 21 | * Returns the root url for all operations in this service. If not set directly in this 22 | * service, will fallback to `ApiConfiguration.rootUrl`. 23 | */ 24 | get rootUrl(): string { 25 | return this._rootUrl || this.config.rootUrl; 26 | } 27 | 28 | /** 29 | * Sets the root URL for API operations in this service. 30 | */ 31 | set rootUrl(rootUrl: string) { 32 | this._rootUrl = rootUrl; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/api/models.ts: -------------------------------------------------------------------------------- 1 | export { BaseProcess } from './models/base-process'; 2 | export { Command } from './models/command'; 3 | export { DefaultRepository } from './models/default-repository'; 4 | export { ExportModel } from './models/export-model'; 5 | export { InternalOpenttdServerConfig } from './models/internal-openttd-server-config'; 6 | export { OpenttdProcess } from './models/openttd-process'; 7 | export { OpenttdServer } from './models/openttd-server'; 8 | export { OpenttdServerConfigGet } from './models/openttd-server-config-get'; 9 | export { OpenttdServerConfigUpdate } from './models/openttd-server-config-update'; 10 | export { OpenttdServerMapper } from './models/openttd-server-mapper'; 11 | export { OpenttdTerminalUpdateEvent } from './models/openttd-terminal-update-event'; 12 | export { Path } from './models/path'; 13 | export { PauseCommand } from './models/pause-command'; 14 | export { ServerFile } from './models/server-file'; 15 | export { ServerFileType } from './models/server-file-type'; 16 | export { ServiceError } from './models/service-error'; 17 | export { ServiceErrorType } from './models/service-error-type'; 18 | export { UnpauseCommand } from './models/unpause-command'; 19 | -------------------------------------------------------------------------------- /ui/src/app/api/models/base-process.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export interface BaseProcess { 4 | processData?: string; 5 | processId?: string; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/api/models/command.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export interface Command { 4 | cmdSend?: boolean; 5 | command?: string; 6 | executed?: boolean; 7 | marker?: string; 8 | rawResult?: string; 9 | type?: string; 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/api/models/default-repository.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { InternalOpenttdServerConfig } from './internal-openttd-server-config'; 4 | import { OpenttdServerMapper } from './openttd-server-mapper'; 5 | import { Path } from './path'; 6 | import { ServerFile } from './server-file'; 7 | export interface DefaultRepository { 8 | configFile?: Path; 9 | openttdConfigDir?: string; 10 | openttdConfigDirPath?: Path; 11 | openttdConfigs?: Array; 12 | openttdSaveDir?: string; 13 | openttdSaveDirPath?: Path; 14 | openttdSaveGames?: Array; 15 | openttdServerConfig?: InternalOpenttdServerConfig; 16 | openttdServerMapper?: OpenttdServerMapper; 17 | serverConfigDir?: string; 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/api/models/export-model.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { OpenttdTerminalUpdateEvent } from './openttd-terminal-update-event'; 4 | import { PauseCommand } from './pause-command'; 5 | import { ServiceError } from './service-error'; 6 | import { UnpauseCommand } from './unpause-command'; 7 | export interface ExportModel { 8 | openttdTerminalUpdateEvent?: OpenttdTerminalUpdateEvent; 9 | pauseCommand?: PauseCommand; 10 | serviceError?: ServiceError; 11 | unpauseCommand?: UnpauseCommand; 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/api/models/internal-openttd-server-config.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { OpenttdServer } from './openttd-server'; 4 | export interface InternalOpenttdServerConfig { 5 | autoSaveMinutes?: number; 6 | numberOfAutoSaveFilesToKeep?: number; 7 | numberOfManuallySaveFilesToKeep?: number; 8 | passwordSha256Hash?: string; 9 | path?: string; 10 | servers?: Array; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-process.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { BaseProcess } from './base-process'; 4 | export interface OpenttdProcess { 5 | config?: string; 6 | id?: string; 7 | port?: number; 8 | process?: BaseProcess; 9 | saveGame?: string; 10 | startServerCommand?: Array; 11 | uiTerminalOpenedByClient?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-server-config-get.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { OpenttdServer } from './openttd-server'; 4 | export interface OpenttdServerConfigGet { 5 | autoSaveMinutes?: number; 6 | numberOfAutoSaveFilesToKeep?: number; 7 | numberOfManuallySaveFilesToKeep?: number; 8 | servers?: Array; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-server-config-update.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export interface OpenttdServerConfigUpdate { 4 | autoSaveMinutes?: number; 5 | numberOfAutoSaveFilesToKeep?: number; 6 | numberOfManuallySaveFilesToKeep?: number; 7 | oldPassword?: string; 8 | password?: string; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-server-mapper.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export interface OpenttdServerMapper { 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-server.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { OpenttdProcess } from './openttd-process'; 4 | import { ServerFile } from './server-file'; 5 | export interface OpenttdServer { 6 | adminPassword?: string; 7 | autoPause?: boolean; 8 | autoSave?: boolean; 9 | currentClients?: number; 10 | currentCompanies?: number; 11 | currentSpectators?: number; 12 | id?: string; 13 | inviteCode?: string; 14 | maxClients?: number; 15 | maxCompanies?: number; 16 | name?: string; 17 | openttdConfig?: ServerFile; 18 | openttdPrivateConfig?: ServerFile; 19 | openttdSecretsConfig?: ServerFile; 20 | password?: string; 21 | paused?: boolean; 22 | port?: number; 23 | process?: OpenttdProcess; 24 | saveGame?: ServerFile; 25 | serverAdminPort?: number; 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/api/models/openttd-terminal-update-event.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export interface OpenttdTerminalUpdateEvent { 4 | clazz?: { 5 | }; 6 | created?: number; 7 | processId?: string; 8 | source?: string; 9 | text?: string; 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/api/models/path.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export type Path = Array; 4 | -------------------------------------------------------------------------------- /ui/src/app/api/models/pause-command.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { DefaultRepository } from './default-repository'; 4 | export interface PauseCommand { 5 | cmdSend?: boolean; 6 | command?: string; 7 | executed?: boolean; 8 | marker?: string; 9 | rawResult?: string; 10 | repository?: DefaultRepository; 11 | type?: string; 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/api/models/server-file-type.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export enum ServerFileType { 4 | SaveGame = 'SAVE_GAME', 5 | Config = 'CONFIG' 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/api/models/server-file.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { ServerFileType } from './server-file-type'; 4 | export interface ServerFile { 5 | created?: number; 6 | exists?: boolean; 7 | lastModified?: number; 8 | name?: string; 9 | ownerId?: string; 10 | ownerName?: string; 11 | path?: string; 12 | type?: ServerFileType; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/api/models/service-error-type.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export enum ServiceErrorType { 4 | ValidationException = 'VALIDATION_EXCEPTION', 5 | RuntimeException = 'RUNTIME_EXCEPTION', 6 | Unknown = 'UNKNOWN' 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/app/api/models/service-error.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { ServiceErrorType } from './service-error-type'; 4 | export interface ServiceError { 5 | message?: string; 6 | stackTrace?: string; 7 | type?: ServiceErrorType; 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/app/api/models/unpause-command.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { DefaultRepository } from './default-repository'; 4 | export interface UnpauseCommand { 5 | cmdSend?: boolean; 6 | command?: string; 7 | executed?: boolean; 8 | marker?: string; 9 | rawResult?: string; 10 | repository?: DefaultRepository; 11 | type?: string; 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/api/services.ts: -------------------------------------------------------------------------------- 1 | export { AuthResourceService } from './services/auth-resource.service'; 2 | export { ChunkUploadResourceService } from './services/chunk-upload-resource.service'; 3 | export { OpenttdServerResourceService } from './services/openttd-server-resource.service'; 4 | -------------------------------------------------------------------------------- /ui/src/app/api/services/chunk-upload-resource.service.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { Injectable } from '@angular/core'; 4 | import { HttpClient, HttpResponse, HttpContext } from '@angular/common/http'; 5 | import { BaseService } from '../base-service'; 6 | import { ApiConfiguration } from '../api-configuration'; 7 | import { StrictHttpResponse } from '../strict-http-response'; 8 | import { RequestBuilder } from '../request-builder'; 9 | import { Observable } from 'rxjs'; 10 | import { map, filter } from 'rxjs/operators'; 11 | 12 | import { ServerFileType } from '../models/server-file-type'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class ChunkUploadResourceService extends BaseService { 18 | constructor( 19 | config: ApiConfiguration, 20 | http: HttpClient 21 | ) { 22 | super(config, http); 23 | } 24 | 25 | /** 26 | * Path part for operation apiChunkUploadPost 27 | */ 28 | static readonly ApiChunkUploadPostPath = '/api/chunk-upload'; 29 | 30 | /** 31 | * This method provides access to the full `HttpResponse`, allowing access to response headers. 32 | * To access only the response body, use `apiChunkUploadPost()` instead. 33 | * 34 | * This method sends `application/octet-stream` and handles request body of type `application/octet-stream`. 35 | */ 36 | apiChunkUploadPost$Response(params?: { 37 | fileName?: string; 38 | fileSize?: number; 39 | offset?: number; 40 | type?: ServerFileType; 41 | context?: HttpContext 42 | body?: Blob 43 | } 44 | ): Observable> { 45 | 46 | const rb = new RequestBuilder(this.rootUrl, ChunkUploadResourceService.ApiChunkUploadPostPath, 'post'); 47 | if (params) { 48 | rb.query('fileName', params.fileName, {}); 49 | rb.query('fileSize', params.fileSize, {}); 50 | rb.query('offset', params.offset, {}); 51 | rb.query('type', params.type, {}); 52 | rb.body(params.body, 'application/octet-stream'); 53 | } 54 | 55 | return this.http.request(rb.build({ 56 | responseType: 'text', 57 | accept: '*/*', 58 | context: params?.context 59 | })).pipe( 60 | filter((r: any) => r instanceof HttpResponse), 61 | map((r: HttpResponse) => { 62 | return (r as HttpResponse).clone({ body: undefined }) as StrictHttpResponse; 63 | }) 64 | ); 65 | } 66 | 67 | /** 68 | * This method provides access to only to the response body. 69 | * To access the full response (for headers, for example), `apiChunkUploadPost$Response()` instead. 70 | * 71 | * This method sends `application/octet-stream` and handles request body of type `application/octet-stream`. 72 | */ 73 | apiChunkUploadPost(params?: { 74 | fileName?: string; 75 | fileSize?: number; 76 | offset?: number; 77 | type?: ServerFileType; 78 | context?: HttpContext 79 | body?: Blob 80 | } 81 | ): Observable { 82 | 83 | return this.apiChunkUploadPost$Response(params).pipe( 84 | map((r: StrictHttpResponse) => r.body as void) 85 | ); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /ui/src/app/api/strict-http-response.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { HttpResponse } from '@angular/common/http'; 4 | 5 | /** 6 | * Constrains the http response to not have the body defined as `T | null`, but `T` only. 7 | */ 8 | export type StrictHttpResponse = HttpResponse & { 9 | readonly body: T; 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {AuthGuard} from './shared/guards/auth.guard'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'servers', 8 | canActivate:[AuthGuard], 9 | loadChildren: () => 10 | import('./servers/feature/servers-shell/servers-shell.module').then((m) => m.ServersShellModule), 11 | }, 12 | { 13 | path: 'settings', 14 | canActivate:[AuthGuard], 15 | loadChildren: () => 16 | import('./settings/feature/settings/settings.module').then((m) => m.SettingsModule), 17 | }, 18 | { 19 | path: 'login', 20 | loadChildren: () => 21 | import('./auth/feature/login/login.module').then((m) => m.LoginModule), 22 | }, 23 | { 24 | path: 'logout', 25 | loadChildren: () => 26 | import('./auth/feature/logout/logout.module').then((m) => m.LogoutModule), 27 | }, 28 | { 29 | path: '', 30 | redirectTo: 'servers', 31 | pathMatch: 'full', 32 | }, 33 | ]; 34 | 35 | @NgModule({ 36 | imports: [RouterModule.forRoot(routes)], 37 | exports: [RouterModule] 38 | }) 39 | export class AppRoutingModule { 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/app.component.scss -------------------------------------------------------------------------------- /ui/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'openttd-server-ui'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('openttd-server-ui'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('openttd-server-ui app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import SunsetTheme from 'highcharts/themes/dark-unica.js'; 3 | import * as Highcharts from 'highcharts'; 4 | import * as HighchartsStock from 'highcharts/highstock'; 5 | import {BackendWebsocketService} from './shared/services/backend-websocket.service'; 6 | import {Router} from '@angular/router'; 7 | import {AuthenticationService} from './shared/services/authentication.service'; 8 | import {SidebarLayoutModel} from './shared/ui/sidebar-layout/sidebar-layout.component'; 9 | import {ApplicationService} from './shared/services/application.service'; 10 | 11 | SunsetTheme(Highcharts); 12 | SunsetTheme(HighchartsStock); 13 | 14 | @Component({ 15 | selector: 'app-root', 16 | templateUrl: './app.component.html', 17 | styleUrls: ['./app.component.scss'] 18 | }) 19 | export class AppComponent implements OnInit { 20 | 21 | window: any; 22 | 23 | sidebarLayoutModel: SidebarLayoutModel = { 24 | entries: [ 25 | { 26 | icon: 'dns', 27 | path: "/servers", 28 | title: "Servers" 29 | }, 30 | { 31 | icon: 'settings', 32 | path: "/settings", 33 | title: "Settings" 34 | }, 35 | ] 36 | } 37 | 38 | constructor(private app: ApplicationService, public auth: AuthenticationService, private router: Router, private backendWebsocketService: BackendWebsocketService) { 39 | this.window = window; 40 | } 41 | 42 | ngOnInit(): void { 43 | this.backendWebsocketService.connect(); 44 | 45 | 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | 4 | import {AppRoutingModule} from './app-routing.module'; 5 | import {AppComponent} from './app.component'; 6 | import {StoreModule} from '@ngrx/store'; 7 | import {StoreDevtoolsModule} from '@ngrx/store-devtools'; 8 | import {environment} from '../environments/environment'; 9 | import {EffectsModule} from '@ngrx/effects'; 10 | import {AppEffects} from './shared/store/effects/app.effects'; 11 | import {metaReducers, reducers} from './shared/store/reducers'; 12 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 13 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 14 | import {HttpAuthInterceptor} from './shared/interceptors/http-auth-interceptor'; 15 | import {SidebarLayoutModule} from './shared/ui/sidebar-layout/sidebar-layout.module'; 16 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 17 | import {AppNotificationsModule} from './shared/ui/app-notifications/app-notifications.module'; 18 | import {DatePipe} from '@angular/common'; 19 | import {LoadingBarHttpClientModule} from "@ngx-loading-bar/http-client"; 20 | import {CustomLoadingBarInterceptor} from "./shared/interceptors/custom-loading-bar.interceptor"; 21 | import {LoadingBarModule} from "@ngx-loading-bar/core"; 22 | 23 | @NgModule({ 24 | declarations: [ 25 | AppComponent 26 | ], 27 | imports: [ 28 | BrowserModule, 29 | AppRoutingModule, 30 | HttpClientModule, 31 | LoadingBarModule, 32 | BrowserAnimationsModule, 33 | EffectsModule.forRoot([AppEffects]), 34 | StoreModule.forRoot(reducers, {metaReducers}), 35 | !environment.production ? StoreDevtoolsModule.instrument( 36 | // { 37 | // actionsBlocklist: [ 38 | // //terminalUpdateEvent.name 39 | // ] 40 | // } 41 | ) : [], 42 | SidebarLayoutModule, 43 | AppNotificationsModule, 44 | MatSnackBarModule, 45 | ], 46 | providers: [ 47 | DatePipe, 48 | {provide: HTTP_INTERCEPTORS, useClass: HttpAuthInterceptor, multi: true}, 49 | { 50 | provide: HTTP_INTERCEPTORS, 51 | useClass: CustomLoadingBarInterceptor, 52 | multi: true, 53 | }, 54 | ], 55 | bootstrap: [AppComponent] 56 | }) 57 | export class AppModule { 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {LoginComponent} from './login.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: LoginComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class LoginRoutingModuleRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Your Company 5 |

Sign in to OpenTTD Server

6 |
7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/auth/feature/login/login.component.scss -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginViewComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoginComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AuthenticationService} from '../../../shared/services/authentication.service'; 3 | 4 | @Component({ 5 | selector: 'app-login', 6 | templateUrl: './login.component.html', 7 | styleUrls: ['./login.component.scss'] 8 | }) 9 | export class LoginComponent implements OnInit { 10 | 11 | username="" 12 | password="" 13 | 14 | constructor(private auth:AuthenticationService) { } 15 | 16 | ngOnInit(): void { 17 | } 18 | 19 | async login() { 20 | await this.auth.login(this.username, this.password); 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {LoginComponent} from './login.component'; 4 | import {LoginRoutingModuleRoutingModule} from './login-routing.module'; 5 | import {FormsModule} from '@angular/forms'; 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | LoginComponent 11 | ], 12 | imports: [ 13 | CommonModule, 14 | LoginRoutingModuleRoutingModule, 15 | FormsModule 16 | ], 17 | exports: [LoginComponent] 18 | }) 19 | export class LoginModule { 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/logout/logout.component.html: -------------------------------------------------------------------------------- 1 |

logout works!

2 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/logout/logout.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/auth/feature/logout/logout.component.scss -------------------------------------------------------------------------------- /ui/src/app/auth/feature/logout/logout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-logout', 5 | templateUrl: './logout.component.html', 6 | styleUrls: ['./logout.component.scss'] 7 | }) 8 | export class LogoutComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/auth/feature/logout/logout.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {LogoutComponent} from './logout.component'; 4 | 5 | 6 | @NgModule({ 7 | declarations: [ 8 | LogoutComponent 9 | ], 10 | imports: [ 11 | CommonModule, 12 | ], 13 | exports: [LogoutComponent] 14 | }) 15 | export class LogoutModule { 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-detail/servers-detail-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {ServersDetailComponent} from './servers-detail.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: ServersDetailComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class ServersDetailRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-detail/servers-detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/servers/feature/servers-detail/servers-detail.component.scss -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-detail/servers-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ServersDetailComponent } from './servers-detail.component'; 4 | 5 | describe('EditServerComponent', () => { 6 | let component: ServersDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ServersDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ServersDetailComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-detail/servers-detail.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {ServersDetailRoutingModule} from './servers-detail-routing.module'; 5 | import {ServersDetailComponent} from './servers-detail.component'; 6 | import {MatFormFieldModule} from '@angular/material/form-field'; 7 | import {MatInputModule} from '@angular/material/input'; 8 | import {ReactiveFormsModule} from '@angular/forms'; 9 | import {MatIconModule} from '@angular/material/icon'; 10 | import {ServerFileSelectModule} from '../../../shared/ui/server-file-select/server-file-select.module'; 11 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 12 | 13 | 14 | @NgModule({ 15 | declarations: [ 16 | ServersDetailComponent 17 | ], 18 | imports: [ 19 | CommonModule, 20 | ServersDetailRoutingModule, 21 | ReactiveFormsModule, 22 | MatFormFieldModule, 23 | MatInputModule, 24 | MatIconModule, 25 | ServerFileSelectModule, 26 | MatSlideToggleModule, 27 | ] 28 | }) 29 | export class ServersDetailModule { } 30 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-overview/servers-overview-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {ServersOverviewComponent} from './servers-overview.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: ServersOverviewComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class ServersOverviewRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-overview/servers-overview.component.html: -------------------------------------------------------------------------------- 1 |

SERVERS

2 |
3 | 6 | 7 |
8 | 9 |
10 | 11 | 12 |
13 |
14 | 19 |
20 | 21 |
22 | 26 |
27 |
28 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-overview/servers-overview.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/servers/feature/servers-overview/servers-overview.component.scss -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-overview/servers-overview.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import {Actions, ofType} from "@ngrx/effects"; 4 | import {MatDialog} from "@angular/material/dialog"; 5 | import * as AppActions from '../../../shared/store/actions/app.actions'; 6 | import {loadProcesses, loadServer} from '../../../shared/store/actions/app.actions'; 7 | import {CreateServerDialogComponent} from '../../ui/create-server-dialog/create-server-dialog.component'; 8 | import {FileUploadDialogComponent} from '../../../shared/ui/file-upload-dialog/file-upload-dialog.component'; 9 | import {ServerFileType} from '../../../api/models/server-file-type'; 10 | 11 | @Component({ 12 | selector: 'app-servers-overview', 13 | templateUrl: './servers-overview.component.html', 14 | styleUrls: ['./servers-overview.component.scss'] 15 | }) 16 | export class ServersOverviewComponent implements OnInit { 17 | init = false; 18 | 19 | constructor(private actions$: Actions, private store: Store<{}>, public dialog: MatDialog) { 20 | } 21 | 22 | ngOnInit(): void { 23 | 24 | this.store.dispatch(loadProcesses({src: ServersOverviewComponent.name})) 25 | 26 | this.actions$.pipe( 27 | ofType(AppActions.saveServerSuccess) 28 | ).subscribe(a => { 29 | this.store.dispatch(loadServer({src: ServersOverviewComponent.name, id: a.id})) 30 | }) 31 | 32 | 33 | } 34 | 35 | createServer() { 36 | const dialogRef = this.dialog.open(CreateServerDialogComponent, { 37 | minWidth: '60%' 38 | }); 39 | 40 | dialogRef.componentInstance.dialogRef = dialogRef; 41 | dialogRef.afterClosed().subscribe(result => { 42 | console.log(`Dialog result: ${result}`); 43 | }); 44 | 45 | } 46 | 47 | uploadSaveGame() { 48 | const dialogRef = this.dialog.open(FileUploadDialogComponent, {minWidth: "800px"}); 49 | dialogRef.componentInstance.dialogRef = dialogRef; 50 | dialogRef.componentInstance.fileType = ServerFileType.SaveGame; 51 | dialogRef.componentInstance.dialogTitle = "UPLOAD OPENTTD SAVEGAMES"; 52 | dialogRef.componentInstance.subTitle = "Info: Don't upload files where the filename contains single/double quotes: ' or \" . This will cause problems!"; 53 | dialogRef.afterClosed().subscribe(result => { 54 | console.log(`Dialog result: ${result}`); 55 | }); 56 | } 57 | 58 | uploadConfig() { 59 | const dialogRef = this.dialog.open(FileUploadDialogComponent, {minWidth: "800px"}); 60 | dialogRef.componentInstance.dialogRef = dialogRef; 61 | dialogRef.componentInstance.fileType = ServerFileType.Config; 62 | dialogRef.componentInstance.dialogTitle = "UPLOAD OPENTTD CONFIGS"; 63 | dialogRef.componentInstance.subTitle = "Info: Don't upload files where the filename contains single/double quotes: ' or \" . This will cause problems!"; 64 | dialogRef.afterClosed().subscribe(result => { 65 | console.log(`Dialog result: ${result}`); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-overview/servers-overview.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ServersOverviewRoutingModule} from './servers-overview-routing.module'; 4 | import {ServersOverviewComponent} from './servers-overview.component'; 5 | import {MatFormFieldModule} from '@angular/material/form-field'; 6 | import {MatSelectModule} from '@angular/material/select'; 7 | import {FormsModule} from '@angular/forms'; 8 | import {OpenttdProcessTerminalDialogModule} from '../../ui/openttd-process-terminal/openttd-process-terminal-dialog.module'; 9 | import {FileUploadDialogModule} from '../../../shared/ui/file-upload-dialog/file-upload-dialog.module'; 10 | import {CreateServerDialogModule} from '../../ui/create-server-dialog/create-server-dialog.module'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import {OpenttdServerGridModule} from '../../ui/openttd-server-grid/openttd-server-grid.module'; 13 | import {MatIconModule} from "@angular/material/icon"; 14 | 15 | 16 | @NgModule({ 17 | declarations: [ 18 | ServersOverviewComponent 19 | ], 20 | imports: [ 21 | CommonModule, 22 | ServersOverviewRoutingModule, 23 | MatButtonModule, 24 | MatFormFieldModule, 25 | MatSelectModule, 26 | FormsModule, 27 | OpenttdProcessTerminalDialogModule, 28 | FileUploadDialogModule, 29 | CreateServerDialogModule, 30 | OpenttdServerGridModule, 31 | MatIconModule 32 | ] 33 | }) 34 | export class ServersOverviewModule { 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-shell/servers-shell-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {RP_ID} from '../../../shared/model/constants'; 4 | 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | loadChildren: () => 10 | import('../servers-overview/servers-overview.module').then( 11 | (m) => m.ServersOverviewModule 12 | ), 13 | }, 14 | { 15 | path: `:${RP_ID}`, 16 | loadChildren: () => 17 | import('../servers-detail/servers-detail.module').then( 18 | (m) => m.ServersDetailModule 19 | ), 20 | } 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule] 26 | }) 27 | export class ServersShellRoutingModule { 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/servers/feature/servers-shell/servers-shell.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {ServersShellRoutingModule} from './servers-shell-routing.module'; 3 | 4 | 5 | @NgModule({ 6 | imports: [ServersShellRoutingModule] 7 | }) 8 | export class ServersShellModule { 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/create-server-dialog/create-server-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/servers/ui/create-server-dialog/create-server-dialog.component.scss -------------------------------------------------------------------------------- /ui/src/app/servers/ui/create-server-dialog/create-server-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ServerFile} from "../../../api/models/server-file"; 3 | import {OpenttdServerResourceService} from "../../../api/services/openttd-server-resource.service"; 4 | import {FormBuilder} from "@angular/forms"; 5 | import {Store} from "@ngrx/store"; 6 | import {ServerFileType} from "../../../api/models/server-file-type"; 7 | import {addServer, loadServerFiles} from '../../../shared/store/actions/app.actions'; 8 | import {selectFiles} from '../../../shared/store/selectors/app.selectors'; 9 | import {MatDialogRef} from '@angular/material/dialog'; 10 | 11 | @Component({ 12 | selector: 'app-create-server-dialog', 13 | templateUrl: './create-server-dialog.component.html', 14 | styleUrls: ['./create-server-dialog.component.scss'] 15 | }) 16 | export class CreateServerDialogComponent implements OnInit { 17 | configFiles: ServerFile[] = [] 18 | saveGameFiles: ServerFile[] = [] 19 | showPassword=false; 20 | showAdminPassword=false; 21 | createServerForm = this.fb.group({ 22 | name: [''], 23 | port: [3979], 24 | adminPort: [3977], 25 | password: [''], 26 | adminPassword: [''], 27 | saveGame: [{}], 28 | config: [{}], 29 | configSecret: [{}], 30 | configPrivate: [{}] 31 | }); 32 | 33 | public dialogRef: MatDialogRef | null = null; 34 | 35 | constructor(private store: Store<{}>, private openttd: OpenttdServerResourceService, private fb: FormBuilder) { 36 | } 37 | 38 | ngOnInit(): void { 39 | this.store.dispatch(loadServerFiles({src: CreateServerDialogComponent.name})) 40 | this.store.select(selectFiles).subscribe(files => { 41 | this.configFiles = []; 42 | this.saveGameFiles = []; 43 | 44 | files.forEach(f => { 45 | if (f.type === ServerFileType.SaveGame) { 46 | this.saveGameFiles.push(f) 47 | } 48 | if (f.type === ServerFileType.Config) { 49 | this.configFiles.push(f) 50 | } 51 | }) 52 | }) 53 | } 54 | 55 | createServer() { 56 | this.store.dispatch(addServer({ 57 | src: CreateServerDialogComponent.name, server: { 58 | name: this.createServerForm.controls.name.value!, 59 | port: this.createServerForm.controls.port.value!, 60 | password: this.createServerForm.controls.password.value!, 61 | serverAdminPort: this.createServerForm.controls.adminPort.value!, 62 | adminPassword: this.createServerForm.controls.adminPassword.value!, 63 | openttdConfig: this.createServerForm.controls.config.value!, 64 | openttdSecretsConfig: this.createServerForm.controls.configSecret.value!, 65 | openttdPrivateConfig: this.createServerForm.controls.configPrivate.value!, 66 | saveGame: this.createServerForm.controls.saveGame.value!, 67 | } 68 | })) 69 | this.dialogRef?.close(true); 70 | } 71 | 72 | selectSaveGame(file: ServerFile) { 73 | if (file && file.name!.length > 0) { 74 | this.createServerForm.controls.saveGame.patchValue(file); 75 | } 76 | } 77 | 78 | selectConfig(file: ServerFile) { 79 | if (file && file.name!.length > 0) { 80 | this.createServerForm.controls.config.patchValue(file); 81 | } 82 | } 83 | 84 | selectSecretConfig(file: ServerFile) { 85 | if (file && file.name!.length > 0) { 86 | this.createServerForm.controls.configSecret.patchValue(file); 87 | } 88 | } 89 | 90 | selectPrivateConfig(file: ServerFile) { 91 | if (file && file.name!.length > 0) { 92 | this.createServerForm.controls.configPrivate.patchValue(file); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/create-server-dialog/create-server-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CreateServerDialogComponent} from './create-server-dialog.component'; 3 | import {ServerFileSelectModule} from '../../../shared/ui/server-file-select/server-file-select.module'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {MatFormFieldModule} from '@angular/material/form-field'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import {MatIconModule} from '@angular/material/icon'; 8 | import {BaseDialogModule} from '../../../shared/ui/base-dialog/base-dialog.module'; 9 | import {NgIf} from "@angular/common"; 10 | 11 | 12 | @NgModule({ 13 | declarations: [ 14 | CreateServerDialogComponent 15 | ], 16 | imports: [ 17 | ServerFileSelectModule, 18 | FormsModule, 19 | ReactiveFormsModule, 20 | MatFormFieldModule, 21 | MatInputModule, 22 | MatIconModule, 23 | BaseDialogModule, 24 | NgIf 25 | 26 | ], 27 | exports: [CreateServerDialogComponent] 28 | }) 29 | export class CreateServerDialogModule { 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-process-terminal/openttd-process-terminal-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-process-terminal/openttd-process-terminal-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/servers/ui/openttd-process-terminal/openttd-process-terminal-dialog.component.scss -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-process-terminal/openttd-process-terminal-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnDestroy} from '@angular/core'; 2 | import {interval, Subject, Subscription} from "rxjs"; 3 | import {Store} from "@ngrx/store"; 4 | import {OpenttdProcess} from 'src/app/api/models'; 5 | import {OpenttdServerResourceService} from '../../../api/services/openttd-server-resource.service'; 6 | import {selectProcesses, selectProcessUpdateEvent} from '../../../shared/store/selectors/app.selectors'; 7 | import {loadProcesses} from "../../../shared/store/actions/app.actions"; 8 | import {MatDialogRef} from "@angular/material/dialog"; 9 | import {ApplicationService} from '../../../shared/services/application.service'; 10 | import {filter} from 'rxjs/operators'; 11 | 12 | @Component({ 13 | selector: 'app-openttd-process-terminal', 14 | templateUrl: './openttd-process-terminal-dialog.component.html', 15 | styleUrls: ['./openttd-process-terminal-dialog.component.scss'] 16 | }) 17 | export class OpenttdProcessTerminalDialogComponent implements AfterViewInit, OnDestroy { 18 | 19 | 20 | public dialogTitle = ''; 21 | 22 | public dialogRef: MatDialogRef | null = null; 23 | 24 | public openttdProcess!: OpenttdProcess; 25 | 26 | consoleInput = new Subject(); 27 | 28 | terminalControl = new Subject(); 29 | showTerminal = false; 30 | 31 | private sub = new Subscription() 32 | 33 | constructor(private app: ApplicationService, private store: Store<{}>, private openttd: OpenttdServerResourceService) { 34 | } 35 | 36 | sendCommand(cmd: string) { 37 | this.openttd.sendTerminalCommand({id: this.openttdProcess.id!, body: cmd}).subscribe(); 38 | } 39 | 40 | ngAfterViewInit(): void { 41 | 42 | setTimeout(() => this.showTerminal = true, 500); 43 | this.store.dispatch(loadProcesses({src: OpenttdProcessTerminalDialogComponent.name})); 44 | 45 | this.sub.add(this.store.select(selectProcesses).subscribe(servers => { 46 | const process = servers.find(s => s.id === this.openttdProcess?.id) 47 | if (process) { 48 | this.openttdProcess = process 49 | setTimeout(() => { 50 | this.terminalControl.next("clear"); 51 | this.consoleInput.next(this.openttdProcess!.process?.processData || "") 52 | }, 1000); 53 | } 54 | })); 55 | 56 | this.sub.add(this.store.select(selectProcessUpdateEvent).pipe(filter(e => e != null)).subscribe(event => { 57 | if (this.openttdProcess && event && event!.processId === this.openttdProcess?.process?.processId) { 58 | this.consoleInput.next(`${event.text}` || ""); 59 | } 60 | })); 61 | 62 | // Tell the backend that the client has an open terminal. This prevents the backen from executing automated commands that otherwise would pollute the terminal 63 | this.sub.add(interval(10000).subscribe(_ => this.openttd.terminalOpenInUi({id: this.openttdProcess.id!}).subscribe({ 64 | error: (e) => { 65 | this.app.handleError(e); 66 | } 67 | }))); 68 | } 69 | 70 | ngOnDestroy(): void { 71 | this.sub.unsubscribe(); 72 | console.log("DESTORY TERMINAL") 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-process-terminal/openttd-process-terminal-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {OpenttdProcessTerminalDialogComponent} from './openttd-process-terminal-dialog.component'; 4 | import {TerminalModule} from '../../../shared/ui/terminal/terminal.module'; 5 | import {BaseDialogModule} from "../../../shared/ui/base-dialog/base-dialog.module"; 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | OpenttdProcessTerminalDialogComponent 11 | ], 12 | imports: [ 13 | CommonModule, 14 | TerminalModule, 15 | BaseDialogModule 16 | ], 17 | exports: [OpenttdProcessTerminalDialogComponent] 18 | }) 19 | export class OpenttdProcessTerminalDialogModule { 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-server-grid/openttd-server-grid.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | tr.example-detail-row { 6 | height: 0; 7 | } 8 | 9 | tr.example-element-row:not(.example-expanded-row):hover { 10 | background: #f1f1f1; 11 | } 12 | 13 | tr.example-element-row:not(.example-expanded-row):active { 14 | background: #efefef; 15 | } 16 | 17 | .example-element-row td { 18 | border-bottom-width: 0; 19 | } 20 | 21 | .example-element-detail { 22 | overflow: hidden; 23 | display: flex; 24 | } 25 | 26 | .example-element-diagram { 27 | min-width: 80px; 28 | border: 2px solid black; 29 | padding: 8px; 30 | font-weight: lighter; 31 | margin: 8px 0; 32 | height: 104px; 33 | } 34 | 35 | .example-element-symbol { 36 | font-weight: bold; 37 | font-size: 40px; 38 | line-height: normal; 39 | } 40 | 41 | .example-element-description { 42 | padding: 16px; 43 | } 44 | 45 | .example-element-description-attribution { 46 | opacity: 0.5; 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-server-grid/openttd-server-grid.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OpenttdServerGridComponent } from './openttd-server-grid.component'; 4 | 5 | describe('OppttdServerTableComponent', () => { 6 | let component: OpenttdServerGridComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ OpenttdServerGridComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(OpenttdServerGridComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-server-grid/openttd-server-grid.component.ts: -------------------------------------------------------------------------------- 1 | import {animate, state, style, transition, trigger} from '@angular/animations'; 2 | import {Component, OnInit} from '@angular/core'; 3 | import {Store} from "@ngrx/store"; 4 | import { 5 | deleteServer, 6 | loadServerConfig, pauseUnpauseServer, 7 | saveServer, 8 | startServer, 9 | stopServer 10 | } from 'src/app/shared/store/actions/app.actions'; 11 | import {OpenttdServer} from '../../../api/models/openttd-server'; 12 | import {selectServers} from '../../../shared/store/selectors/app.selectors'; 13 | import {MatDialog} from "@angular/material/dialog"; 14 | import {OpenttdProcessTerminalDialogComponent} from "../openttd-process-terminal/openttd-process-terminal-dialog.component"; 15 | 16 | @Component({ 17 | selector: 'app-openttd-server-grid', 18 | templateUrl: './openttd-server-grid.component.html', 19 | styleUrls: ['./openttd-server-grid.component.scss'], 20 | animations: [ 21 | trigger('detailExpand', [ 22 | state('collapsed', style({height: '0px', minHeight: '0'})), 23 | state('expanded', style({height: '*'})), 24 | transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), 25 | ]), 26 | ], 27 | }) 28 | export class OpenttdServerGridComponent implements OnInit { 29 | dataSource: OpenttdServer[] = [] 30 | columnsToDisplay = ['name', 'port', 'config', 'startSaveGame', 'saveGame', 'autoSaveGame', 'actions']; 31 | columnsToDisplayWithExpand = [...this.columnsToDisplay]; 32 | expandedElement: OpenttdServer | null | undefined; 33 | showTerminal = false; 34 | 35 | constructor(private store: Store, public dialog: MatDialog) { 36 | } 37 | 38 | ngOnInit(): void { 39 | this.store.dispatch(loadServerConfig({src: OpenttdServerGridComponent.name})) 40 | this.store.select(selectServers).subscribe(server => { 41 | 42 | this.dataSource = server 43 | }) 44 | } 45 | 46 | T(v: any): OpenttdServer { 47 | return v as OpenttdServer; 48 | } 49 | 50 | 51 | startServer(server: OpenttdServer) { 52 | this.store.dispatch(startServer({src: OpenttdServerGridComponent.name, id: server.id!})) 53 | } 54 | 55 | stopServer(server: OpenttdServer) { 56 | this.store.dispatch(stopServer({src: OpenttdServerGridComponent.name, id: server.id!})) 57 | } 58 | 59 | trackBy(index: number, item: OpenttdServer) { 60 | return item.name; 61 | } 62 | 63 | loadTerminal(server: OpenttdServer | undefined) { 64 | console.log(">>>>>>>>>>>###", server) 65 | if (server && server.process) { 66 | const dialogRef = this.dialog.open(OpenttdProcessTerminalDialogComponent, { 67 | minWidth: '60%', 68 | }); 69 | 70 | dialogRef.componentInstance.dialogRef = dialogRef; 71 | dialogRef.componentInstance.dialogTitle = `Server: ${server.name}`; 72 | dialogRef.componentInstance.openttdProcess = server.process; 73 | dialogRef.afterClosed().subscribe(result => { 74 | console.log(`Dialog result: ${result}`); 75 | }); 76 | } 77 | 78 | } 79 | 80 | deleteServer(server: OpenttdServer) { 81 | this.store.dispatch(deleteServer({src: OpenttdServerGridComponent.name, id: server.id!})) 82 | } 83 | 84 | save(server: OpenttdServer) { 85 | this.store.dispatch(saveServer({src: OpenttdServerGridComponent.name, id: server.id!})) 86 | } 87 | 88 | pauseServer(server: OpenttdServer) { 89 | this.store.dispatch(pauseUnpauseServer({src: OpenttdServerGridComponent.name, id: server.id!})) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ui/src/app/servers/ui/openttd-server-grid/openttd-server-grid.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {OpenttdServerGridComponent} from './openttd-server-grid.component'; 4 | import {MatTableModule} from '@angular/material/table'; 5 | import {MatIconModule} from '@angular/material/icon'; 6 | import {OpenttdProcessTerminalDialogModule} from '../openttd-process-terminal/openttd-process-terminal-dialog.module'; 7 | import {MatDialogModule} from "@angular/material/dialog"; 8 | import {MatTooltipModule} from '@angular/material/tooltip'; 9 | import {RouterLink} from '@angular/router'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [ 14 | OpenttdServerGridComponent 15 | ], 16 | imports: [ 17 | CommonModule, 18 | MatTableModule, 19 | MatIconModule, 20 | OpenttdProcessTerminalDialogModule, 21 | MatDialogModule, 22 | MatTooltipModule, 23 | RouterLink 24 | ], 25 | exports: [OpenttdServerGridComponent] 26 | }) 27 | export class OpenttdServerGridModule { 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/services: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/services -------------------------------------------------------------------------------- /ui/src/app/settings/feature/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {SettingsComponent} from './settings.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SettingsComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SettingsRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/settings/feature/settings/settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/settings/feature/settings/settings.component.scss -------------------------------------------------------------------------------- /ui/src/app/settings/feature/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SettingsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/settings/feature/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {SettingsComponent} from './settings.component'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {MatFormFieldModule} from '@angular/material/form-field'; 6 | import {MatInputModule} from '@angular/material/input'; 7 | import {MatIconModule} from '@angular/material/icon'; 8 | import {SettingsRoutingModule} from './settings-routing.module'; 9 | 10 | 11 | @NgModule({ 12 | declarations: [ 13 | SettingsComponent 14 | ], 15 | imports: [ 16 | CommonModule, 17 | FormsModule, 18 | ReactiveFormsModule, 19 | MatFormFieldModule, 20 | MatInputModule, 21 | MatIconModule, 22 | SettingsRoutingModule 23 | ], 24 | exports: [ 25 | SettingsComponent 26 | ] 27 | }) 28 | export class SettingsModule { 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/settings/ui/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/settings/ui/.gitkeep -------------------------------------------------------------------------------- /ui/src/app/shared/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; 3 | import {AuthenticationService} from "../services/authentication.service"; 4 | import {interval} from "rxjs"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | constructor( 11 | private router: Router, 12 | private authenticationService: AuthenticationService 13 | ) { 14 | interval(30000).subscribe(_ => { 15 | this.authenticationService.isLoggedIn().then(loggedIn => { 16 | console.log(loggedIn) 17 | if (!loggedIn) { 18 | this.router.navigate(['/login']); 19 | } 20 | }) 21 | }); 22 | } 23 | 24 | async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 25 | if (await this.authenticationService.isLoggedIn()) { 26 | // logged in so return true 27 | return true; 28 | } 29 | // not logged in so redirect to login page with the return url 30 | this.router.navigate(['/login']); 31 | return false; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/shared/interceptors/custom-loading-bar.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {LoadingBarService} from '@ngx-loading-bar/core'; 2 | import {Injectable} from '@angular/core'; 3 | import {HttpContextToken, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 4 | import {Observable} from 'rxjs'; 5 | import {finalize, tap} from 'rxjs/operators'; 6 | import {AuthResourceService} from "../../api/services/auth-resource.service"; 7 | import {OpenttdServerResourceService} from '../../api/services/openttd-server-resource.service'; 8 | 9 | export const NGX_LOADING_BAR_IGNORED = new HttpContextToken(() => false); 10 | declare const ngDevMode: boolean; 11 | 12 | @Injectable() 13 | export class CustomLoadingBarInterceptor implements HttpInterceptor { 14 | constructor(private loader: LoadingBarService) { 15 | } 16 | 17 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 18 | 19 | // https://github.com/angular/angular/issues/18155 20 | if (req.headers.has('ignoreLoadingBar')) { 21 | if (typeof ngDevMode === 'undefined' || ngDevMode) { 22 | console.warn( 23 | `Using http headers ('ignoreLoadingBar') to ignore loading bar is deprecated. Use HttpContext instead: 'context: new HttpContext().set(NGX_LOADING_BAR_IGNORED, true)'`, 24 | ); 25 | } 26 | 27 | return next.handle(req.clone({headers: req.headers.delete('ignoreLoadingBar')})); 28 | } 29 | if (req.context.get(NGX_LOADING_BAR_IGNORED) === true) { 30 | return next.handle(req); 31 | } 32 | 33 | // Ignore loading bar if a specific url is requested 34 | if ( 35 | // ignore on auth and terminal check 36 | req.url.endsWith(AuthResourceService.ApiAuthVerifyLoginPostPath) 37 | || req.url.endsWith(OpenttdServerResourceService.TerminalOpenInUiPath) 38 | ) { 39 | return next.handle(req); 40 | } 41 | 42 | 43 | let started = false; 44 | const ref = this.loader.useRef('http'); 45 | return next.handle(req).pipe( 46 | tap(() => { 47 | if (!started) { 48 | ref.start(); 49 | started = true; 50 | } 51 | }), 52 | finalize(() => started && ref.complete()), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/app/shared/interceptors/http-auth-interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {HEADER_OPENTTD_SERVER_SESSION_ID} from '../model/constants'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class HttpAuthInterceptor implements HttpInterceptor { 10 | intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { 11 | if (localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID)) { 12 | const headers: any = {} 13 | headers[HEADER_OPENTTD_SERVER_SESSION_ID] = localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID); 14 | return next.handle(httpRequest.clone({setHeaders: headers})); 15 | } else { 16 | return next.handle(httpRequest); 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/constants.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_OPENTTD_SERVER_SESSION_ID = "X-OPENTTD_SERVER_SESSION_ID" 2 | 3 | // Route parameters = RP_ 4 | export const RP_ID = "id" 5 | -------------------------------------------------------------------------------- /ui/src/app/shared/services/application.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import {createAlert, removeAlert} from '../store/actions/app.actions'; 4 | import {MatSnackBar} from '@angular/material/snack-bar'; 5 | import {AppNotificationsComponent} from '../ui/app-notifications/app-notifications.component'; 6 | import {HttpErrorResponse} from '@angular/common/http'; 7 | import {ServiceError} from '../../api/models/service-error'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ApplicationService { 13 | private messageId = 1; 14 | 15 | constructor(private store: Store<{}>, private _snackBar: MatSnackBar) { 16 | // Set up the snackbar but the size is set to 0px, so it is not visible 17 | this._snackBar.openFromComponent(AppNotificationsComponent, {verticalPosition: 'top', panelClass: 'snackbar'}) 18 | } 19 | 20 | createErrorMessage(message: string, stacktrace?: string): void { 21 | 22 | this.messageId++; 23 | 24 | this.store.dispatch( 25 | createAlert({ 26 | src: ApplicationService.name, 27 | alert: { 28 | id: this.messageId + '', 29 | message: 'Error: ' + message, 30 | type: 'error', 31 | stacktrace 32 | }, 33 | }) 34 | ); 35 | 36 | } 37 | 38 | createInfoMessage(message: string, millis?: number): void { 39 | this.messageId++; 40 | const msgid = this.messageId; 41 | 42 | this.store.dispatch( 43 | createAlert({ 44 | src: ApplicationService.name, 45 | alert: { 46 | id: msgid + '', 47 | message: 'Info: ' + message, 48 | type: 'info', 49 | }, 50 | }) 51 | ); 52 | 53 | if (millis && millis >= 0) { 54 | setTimeout(() => { 55 | this.store.dispatch( 56 | removeAlert({ 57 | src: ApplicationService.name, 58 | alertId: msgid + `` 59 | })) 60 | }, millis) 61 | } 62 | } 63 | 64 | createWarningMessage(message: string, stacktrace?: string): void { 65 | this.messageId++; 66 | this.store.dispatch( 67 | createAlert({ 68 | src: ApplicationService.name, 69 | alert: { 70 | id: this.messageId + '', 71 | message: 'Warning: ' + message, 72 | type: 'warning', 73 | stacktrace 74 | }, 75 | }) 76 | ); 77 | } 78 | 79 | handleError(e: HttpErrorResponse): void { 80 | if (e.error) { 81 | let error = (e.error as ServiceError); 82 | this.createErrorMessage(`${error.message}`,error.stackTrace) 83 | } else { 84 | this.createErrorMessage(`HTTP Error. See console log`) 85 | } 86 | console.error(e); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ui/src/app/shared/services/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpErrorResponse, HttpResponse} from "@angular/common/http"; 3 | import {AuthResourceService} from '../../api/services/auth-resource.service'; 4 | import {environment} from '../../../environments/environment'; 5 | import {firstValueFrom} from 'rxjs'; 6 | import {HEADER_OPENTTD_SERVER_SESSION_ID} from '../model/constants'; 7 | import {Router} from '@angular/router'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthenticationService { 13 | 14 | 15 | constructor(private router: Router, private http: HttpClient) { 16 | } 17 | 18 | async login(username: string, password: string): Promise { 19 | 20 | let httpResponse = await firstValueFrom(this.http.post(`${environment.baseUrl}${AuthResourceService.ApiAuthLoginPostPath}`, null, { 21 | observe: 'response', 22 | headers: {Authorization: "Basic " + window.btoa(`${username}:${password}`)} 23 | })); 24 | const session = httpResponse.headers.get(HEADER_OPENTTD_SERVER_SESSION_ID) 25 | if (session && session.length > 0) { 26 | localStorage.setItem(HEADER_OPENTTD_SERVER_SESSION_ID, session) 27 | await this.router.navigateByUrl("/") 28 | } 29 | } 30 | 31 | async isLoggedIn(): Promise { 32 | if (localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID)) { 33 | const headers: any = {} 34 | headers[HEADER_OPENTTD_SERVER_SESSION_ID] = localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID); 35 | let httpResponse: HttpResponse = await firstValueFrom(this.http.post(`${environment.baseUrl}${AuthResourceService.ApiAuthVerifyLoginPostPath}`, null, { 36 | observe: 'response', 37 | headers 38 | })).catch(e => { 39 | return e; 40 | }) 41 | 42 | if (httpResponse?.status === 200) { 43 | return true; 44 | } else { 45 | return false; 46 | } 47 | } else { 48 | return false; 49 | } 50 | 51 | } 52 | 53 | logout() { 54 | if (localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID)) { 55 | const headers: any = {} 56 | headers[HEADER_OPENTTD_SERVER_SESSION_ID] = localStorage.getItem(HEADER_OPENTTD_SERVER_SESSION_ID); 57 | this.http.post(`${environment.baseUrl}${AuthResourceService.ApiAuthLogoutPostPath}`, null, { 58 | headers 59 | }).subscribe(_ => { 60 | this.router.navigateByUrl("/login") 61 | localStorage.removeItem(HEADER_OPENTTD_SERVER_SESSION_ID) 62 | }); 63 | } else { 64 | this.router.navigateByUrl("/login") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/shared/services/backend-websocket.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import ReconnectingWebSocket from 'reconnecting-websocket'; 3 | import {Store} from '@ngrx/store'; 4 | import {processUpdateEvent} from '../store/actions/app.actions'; 5 | import {environment} from '../../../environments/environment'; 6 | import {OpenttdTerminalUpdateEvent} from '../../api/models/openttd-terminal-update-event'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class BackendWebsocketService { 12 | private webSocket: ReconnectingWebSocket | null = null; 13 | 14 | constructor(private store: Store<{}>) { 15 | } 16 | 17 | connect(): void { 18 | this.webSocket = new ReconnectingWebSocket(environment.wsServerRoot + `/data-stream`, [], { 19 | minReconnectionDelay: 500 20 | }) 21 | 22 | this.webSocket.onopen = (event) => { 23 | // console.log("onopen", event); 24 | }; 25 | 26 | this.webSocket.onerror = (event) => { 27 | // console.log("onerror", event); 28 | 29 | }; 30 | 31 | this.webSocket.onclose = (event) => { 32 | // console.log("onclose", event); 33 | }; 34 | 35 | this.webSocket.onmessage = (event) => { 36 | if (JSON.parse(event.data)?.ping) { 37 | console.log("received Ping", event); 38 | } else { 39 | const msg = JSON.parse(event.data); 40 | if (msg._type === "OpenttdTerminalUpdateEvent") { 41 | this.store.dispatch(processUpdateEvent({src: BackendWebsocketService.name, event: msg as OpenttdTerminalUpdateEvent})) 42 | } 43 | } 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/shared/services/utils.service.ts: -------------------------------------------------------------------------------- 1 | import {DatePipe} from '@angular/common'; 2 | import {Injectable} from '@angular/core'; 3 | import {ServerFile} from '../../api/models/server-file'; 4 | import {ServerFileType} from '../../api/models/server-file-type'; 5 | 6 | 7 | export const saveData = (data: any, fileName: string) => { 8 | var a: any = document.createElement("a"); 9 | document.body.appendChild(a); 10 | a.style = "display: none"; 11 | const blob = new Blob([data], {type: "application/octet-stream"}); 12 | const url = window.URL.createObjectURL(blob); 13 | a.href = url; 14 | a.download = fileName; 15 | a.click(); 16 | window.URL.revokeObjectURL(url); 17 | }; 18 | 19 | export const clone = (item: T): T => { 20 | if (item) { 21 | return JSON.parse(JSON.stringify(item)); 22 | } 23 | return item; 24 | } 25 | 26 | export const truncateString = (value: string, maxLength: number) => { 27 | if (!value) { 28 | return value; 29 | } 30 | return (value.length > maxLength) ? value.slice(0, maxLength - 1) + '…' : value; 31 | }; 32 | 33 | 34 | @Injectable({ 35 | providedIn: 'root' 36 | }) 37 | export class UtilsService { 38 | 39 | constructor(private datePipe: DatePipe) { 40 | 41 | } 42 | 43 | serverFileNameBeautifier(file: ServerFile) { 44 | 45 | console.log(file) 46 | if (file && file.type === ServerFileType.SaveGame && file?.lastModified && file.name) { 47 | 48 | const fileEnding = file.name.split('.').pop() 49 | const truncPrefix = truncateString(file.ownerName || file.name, 16); 50 | const data = this.datePipe.transform(file.lastModified, 'MMM d, y, HH:mm:ss'); 51 | return `${truncPrefix}-${data}.${fileEnding}`; 52 | } else if (file && file.type === ServerFileType.Config) { 53 | file.name; 54 | } 55 | return ''; 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/app/shared/store/reducers/app.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createReducer, on} from '@ngrx/store'; 2 | import * as AppActions from '../actions/app.actions'; 3 | import {OpenttdServer} from '../../../api/models/openttd-server'; 4 | import {OpenttdProcess} from '../../../api/models/openttd-process'; 5 | import {ServerFile} from '../../../api/models/server-file'; 6 | import {OpenttdTerminalUpdateEvent} from '../../../api/models/openttd-terminal-update-event'; 7 | import {OpenttdServerConfigGet} from '../../../api/models/openttd-server-config-get'; 8 | 9 | export interface AppAlert { 10 | id: string; 11 | type: 'warning' | 'info' | 'success' | 'error'; 12 | message: string; 13 | stacktrace?: string; 14 | } 15 | 16 | 17 | export const appFeatureKey = 'app'; 18 | 19 | 20 | export interface State { 21 | config?: OpenttdServerConfigGet 22 | servers: OpenttdServer[] 23 | server?: OpenttdServer 24 | processes: OpenttdProcess[]; 25 | files: ServerFile[]; 26 | processUpdateEvent?: OpenttdTerminalUpdateEvent; 27 | alerts: AppAlert[]; 28 | } 29 | 30 | export const initialState: State = { 31 | servers: [], 32 | processes: [], 33 | files: [], 34 | alerts: [] 35 | }; 36 | 37 | export const reducer = createReducer( 38 | initialState, 39 | on(AppActions.createAlert, (state, action) => { 40 | const newState = {...state, alerts: state.alerts.concat()}; 41 | newState.alerts.push(action.alert); 42 | return newState; 43 | }), 44 | on(AppActions.removeAlert, (state, action) => { 45 | return { 46 | ...state, 47 | alerts: state.alerts.filter((a) => a.id !== action.alertId), 48 | }; 49 | }), 50 | 51 | on(AppActions.processUpdateEvent, (state, action) => { 52 | return { 53 | ...state, 54 | processUpdateEvent: action.event 55 | } 56 | }), 57 | 58 | on(AppActions.loadProcessesSuccess, (state, action) => { 59 | 60 | 61 | return { 62 | ...state, 63 | processes: action.result 64 | } 65 | 66 | }), 67 | 68 | on(AppActions.loadServerFilesSuccess, (state, action) => { 69 | 70 | 71 | return { 72 | ...state, 73 | files: action.files 74 | } 75 | 76 | }), 77 | 78 | on(AppActions.loadServerConfigSuccess, AppActions.patchServerConfigSuccess, (state, action) => { 79 | return { 80 | ...state, 81 | config: action.config, 82 | servers: action.config.servers! 83 | 84 | } 85 | }), 86 | 87 | on(AppActions.addServerSuccess, (state, action) => { 88 | return { 89 | ...state, 90 | servers: state.servers.concat(action.server) 91 | } 92 | }), 93 | 94 | on(AppActions.updateServerSuccess, AppActions.startServerSuccess, AppActions.loadServerSuccess 95 | , AppActions.stopServerSuccess, AppActions.pauseUnpauseServerSuccess, (state, action) => { 96 | return { 97 | ...state, 98 | server: action.server, 99 | servers: state.servers.map(s => { 100 | if (s.id === action.server.id) { 101 | return action.server; 102 | } 103 | return s; 104 | }) 105 | } 106 | }), 107 | 108 | on(AppActions.deleteServerSuccess, (state, action) => { 109 | return { 110 | ...state, 111 | servers: state.servers.filter(s => { 112 | return (s.id !== action.id) 113 | }) 114 | } 115 | }), 116 | 117 | on(AppActions.startServerSuccess, (state, action) => { 118 | return { 119 | ...state, 120 | processes: state.processes.concat(action.server.process!) 121 | } 122 | }), 123 | ); 124 | -------------------------------------------------------------------------------- /ui/src/app/shared/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducerMap, MetaReducer} from '@ngrx/store'; 2 | import * as fromWorkers from './app.reducer'; 3 | import {environment} from '../../../../environments/environment'; 4 | 5 | 6 | export interface State { 7 | 8 | [fromWorkers.appFeatureKey]: fromWorkers.State; 9 | 10 | 11 | } 12 | 13 | export const reducers: ActionReducerMap = { 14 | 15 | [fromWorkers.appFeatureKey]: fromWorkers.reducer, 16 | 17 | }; 18 | 19 | 20 | export const metaReducers: MetaReducer[] = !environment.production ? [] : []; 21 | -------------------------------------------------------------------------------- /ui/src/app/shared/store/selectors/app.selectors.ts: -------------------------------------------------------------------------------- 1 | import {createFeatureSelector, createSelector} from '@ngrx/store'; 2 | import * as fromApp from '../reducers/app.reducer'; 3 | 4 | export const selectAppState = createFeatureSelector( 5 | fromApp.appFeatureKey 6 | ); 7 | 8 | export const selectAlerts = createSelector(selectAppState, (state) => state.alerts); 9 | 10 | 11 | export const selectProcesses = createSelector(selectAppState, (state) => state.processes); 12 | export const selectProcessUpdateEvent = createSelector(selectAppState, (state) => state.processUpdateEvent); 13 | export const selectServers = createSelector(selectAppState, (state) => state.servers); 14 | export const selectServer = createSelector(selectAppState, (state) => state.server); 15 | export const selectConfig = createSelector(selectAppState, (state) => state.config); 16 | 17 | export const selectFiles = createSelector(selectAppState, (state) => state.files); 18 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/app-notifications/app-notifications.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | warning 6 |
7 |
8 |

9 | {{alert.message}} 10 |

11 | 14 |
15 |
16 |
17 | close 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 | check_circle 27 |
28 |
29 |

30 | {{alert.message}} 31 |

32 |
33 |
34 |
35 | close 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | cancel 45 |
46 |
47 |

48 | {{alert.message}} 49 |

50 | 53 |
54 |
55 |
56 | close 57 |
58 |
59 |
60 |
61 | 62 |
63 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/app-notifications/app-notifications.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/shared/ui/app-notifications/app-notifications.component.scss -------------------------------------------------------------------------------- /ui/src/app/shared/ui/app-notifications/app-notifications.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AppNotificationsComponent } from './app-notifications.component'; 4 | 5 | describe('AppNotificationsComponent', () => { 6 | let component: AppNotificationsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AppNotificationsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AppNotificationsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/app-notifications/app-notifications.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import {selectAlerts} from '../../store/selectors/app.selectors'; 4 | import {AppAlert} from '../../store/reducers/app.reducer'; 5 | import {removeAlert} from '../../store/actions/app.actions'; 6 | 7 | @Component({ 8 | selector: 'app-app-notifications', 9 | templateUrl: './app-notifications.component.html', 10 | styleUrls: ['./app-notifications.component.scss'], 11 | }) 12 | export class AppNotificationsComponent implements OnInit { 13 | 14 | alerts: AppAlert[] = []; 15 | 16 | constructor(private store: Store<{}>) { 17 | } 18 | 19 | ngOnInit(): void { 20 | this.store.select(selectAlerts).subscribe(alerts => { 21 | this.alerts = JSON.parse(JSON.stringify(alerts)); 22 | }) 23 | } 24 | 25 | remove(messageId: string) { 26 | this.store.dispatch( 27 | removeAlert({ 28 | src: AppNotificationsComponent.name, 29 | alertId: messageId 30 | })) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/app-notifications/app-notifications.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AppNotificationsComponent } from './app-notifications.component'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppNotificationsComponent 11 | ], 12 | imports: [ 13 | CommonModule, 14 | MatIconModule 15 | ] 16 | }) 17 | export class AppNotificationsModule { } 18 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/base-dialog/base-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{title}} 9 |

{{subTitle}}

10 |
11 |
12 | 13 | close 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 30 | 31 |
32 |
33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/base-dialog/base-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/shared/ui/base-dialog/base-dialog.component.scss -------------------------------------------------------------------------------- /ui/src/app/shared/ui/base-dialog/base-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BaseDialogComponent } from './base-dialog.component'; 4 | 5 | describe('BaseDialogComponent', () => { 6 | let component: BaseDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BaseDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(BaseDialogComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/base-dialog/base-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core'; 2 | import {MatDialogRef} from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-base-dialog', 6 | templateUrl: './base-dialog.component.html', 7 | styleUrls: ['./base-dialog.component.scss'] 8 | }) 9 | export class BaseDialogComponent implements OnInit { 10 | 11 | @Input() 12 | title: string | undefined = ""; 13 | 14 | @Input() 15 | subTitle: string | undefined = ""; 16 | 17 | @Input() 18 | bodyTemplate: TemplateRef | null = null; 19 | 20 | @Input() 21 | footerTemplate: TemplateRef | null = null 22 | 23 | @Input() 24 | dialogRef: MatDialogRef | null = null; 25 | 26 | constructor() { 27 | } 28 | 29 | ngOnInit(): void { 30 | } 31 | 32 | 33 | close(result: boolean) { 34 | if (this.dialogRef) { 35 | this.dialogRef.close(result) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/base-dialog/base-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BaseDialogComponent } from './base-dialog.component'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | BaseDialogComponent 11 | ], 12 | exports: [ 13 | BaseDialogComponent 14 | ], 15 | imports: [ 16 | CommonModule, 17 | MatIconModule 18 | ] 19 | }) 20 | export class BaseDialogModule { } 21 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/file-upload-dialog/file-upload-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/shared/ui/file-upload-dialog/file-upload-dialog.component.scss -------------------------------------------------------------------------------- /ui/src/app/shared/ui/file-upload-dialog/file-upload-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FileUploadDialogComponent } from './file-upload-dialog.component'; 4 | 5 | describe('FileUploadDialogComponent', () => { 6 | let component: FileUploadDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FileUploadDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(FileUploadDialogComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/file-upload-dialog/file-upload-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {FileUploadDialogComponent} from './file-upload-dialog.component'; 4 | import {JustUploadModule} from '@andreashauschild/just-upload'; 5 | import {MatIconModule} from '@angular/material/icon'; 6 | import {MatDialogContent, MatDialogModule} from '@angular/material/dialog'; 7 | import {FormsModule} from '@angular/forms'; 8 | import {MatButtonModule} from '@angular/material/button'; 9 | import {BaseDialogModule} from '../base-dialog/base-dialog.module'; 10 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 11 | 12 | 13 | @NgModule({ 14 | declarations: [ 15 | FileUploadDialogComponent 16 | ], 17 | imports: [ 18 | CommonModule, 19 | JustUploadModule, 20 | MatIconModule, 21 | MatDialogModule, 22 | FormsModule, 23 | MatIconModule, 24 | MatButtonModule, 25 | BaseDialogModule, 26 | MatProgressBarModule 27 | ], 28 | exports: [FileUploadDialogComponent, MatDialogContent] 29 | }) 30 | export class FileUploadDialogModule { 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/server-file-select/server-file-select.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{label}} 4 | 9 | 11 | 12 | 13 | 14 | 15 |
16 |
{{file?.ownerName}}
17 |
{{file.name}}
18 |
{{file.lastModified | date: 'MMM d, y, HH:mm:ss'}}
19 |
20 |
21 |
22 | {{hintStart}} 23 | {{hintEnd}} 24 |
25 | download_for_offline 26 |
27 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/server-file-select/server-file-select.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/shared/ui/server-file-select/server-file-select.component.scss -------------------------------------------------------------------------------- /ui/src/app/shared/ui/server-file-select/server-file-select.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; 2 | import {FormControl} from '@angular/forms'; 3 | import {Observable, tap} from 'rxjs'; 4 | import {filter, map, startWith} from 'rxjs/operators'; 5 | import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete"; 6 | import {ServerFile} from '../../../api/models/server-file'; 7 | import {OpenttdServerResourceService} from '../../../api/services/openttd-server-resource.service'; 8 | import {saveData} from '../../services/utils.service'; 9 | import {ServerFileType} from '../../../api/models/server-file-type'; 10 | 11 | 12 | @Component({ 13 | selector: 'app-server-file-select', 14 | templateUrl: './server-file-select.component.html', 15 | styleUrls: ['./server-file-select.component.scss'] 16 | }) 17 | export class ServerFileSelectComponent implements OnInit, OnChanges { 18 | 19 | 20 | myControl = new FormControl(undefined); 21 | 22 | @Output() 23 | selectedFileEvent = new EventEmitter(); 24 | 25 | @Output() 26 | selectedFileDownloadEvent = new EventEmitter(); 27 | 28 | @Input() 29 | files: ServerFile[] = []; 30 | 31 | @Input() 32 | label: string = ''; 33 | 34 | @Input() 35 | hintStart: string = ''; 36 | 37 | @Input() 38 | hintEnd: string = ''; 39 | 40 | filteredFiles: Observable | undefined; 41 | 42 | @Input() 43 | selectedFile: ServerFile | undefined; 44 | 45 | constructor(private openttdService: OpenttdServerResourceService) { 46 | 47 | } 48 | 49 | ngOnInit() { 50 | this.selectSelectedFile(); 51 | } 52 | 53 | 54 | ngOnChanges(changes: SimpleChanges): void { 55 | 56 | this.filteredFiles = this.myControl.valueChanges.pipe( 57 | tap(v => console.log("FILTER", v)), 58 | // selected value can be a file, but a input is always a string. if its a file with name property it is filtered out 59 | filter(value => !value?.hasOwnProperty('name')), 60 | startWith(''), 61 | map(value => this._filter(value+'')), 62 | ); 63 | 64 | this.selectSelectedFile(); 65 | } 66 | 67 | select($event: MatAutocompleteSelectedEvent) { 68 | this.selectedFile = $event.option.value; 69 | this.selectedFileEvent.emit($event.option.value); 70 | } 71 | 72 | fileToString(file: any) { 73 | if (file) { 74 | 75 | return file.name; 76 | } 77 | return ''; 78 | } 79 | 80 | private _filter(fileName: string): ServerFile[] { 81 | 82 | const filterValue = fileName.toLowerCase(); 83 | 84 | return this.files.filter(files => files.name!.toLowerCase().includes(filterValue) || files.ownerName?.toLowerCase().includes(filterValue)); 85 | } 86 | 87 | private selectSelectedFile() { 88 | if (this.selectedFile) { 89 | this.myControl.setValue(this.selectedFile); 90 | } 91 | } 92 | 93 | download() { 94 | if (this.selectedFile) { 95 | switch (this.selectedFile.type) { 96 | case ServerFileType.Config: { 97 | this.openttdService.downloadOpenttdConfig({fileName: this.selectedFile?.name}).subscribe(f => { 98 | saveData(f, this.selectedFile?.name!); 99 | }) 100 | break 101 | } 102 | case ServerFileType.SaveGame: { 103 | this.openttdService.downloadSaveGame({fileName: this.selectedFile?.name}).subscribe(f => { 104 | saveData(f, this.selectedFile?.name!); 105 | }) 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/server-file-select/server-file-select.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule, DatePipe} from '@angular/common'; 3 | import {JustUploadModule} from '@andreashauschild/just-upload'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | import {MatDialogModule} from '@angular/material/dialog'; 6 | import {ServerFileSelectComponent} from './server-file-select.component'; 7 | import {MatSelectModule} from '@angular/material/select'; 8 | import {MatInputModule} from '@angular/material/input'; 9 | import {MatFormFieldModule} from '@angular/material/form-field'; 10 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 11 | import {ReactiveFormsModule} from '@angular/forms'; 12 | import {MatTooltipModule} from '@angular/material/tooltip'; 13 | 14 | 15 | @NgModule({ 16 | declarations: [ 17 | ServerFileSelectComponent 18 | ], 19 | imports: [ 20 | CommonModule, 21 | JustUploadModule, 22 | MatIconModule, 23 | MatDialogModule, 24 | MatSelectModule, 25 | MatInputModule, 26 | MatFormFieldModule, 27 | MatAutocompleteModule, 28 | ReactiveFormsModule, 29 | MatTooltipModule, 30 | ], 31 | 32 | exports:[ServerFileSelectComponent] 33 | }) 34 | export class ServerFileSelectModule { 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/sidebar-layout/sidebar-layout.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/sidebar-layout/sidebar-layout.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/app/shared/ui/sidebar-layout/sidebar-layout.component.scss -------------------------------------------------------------------------------- /ui/src/app/shared/ui/sidebar-layout/sidebar-layout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarLayoutComponent } from './sidebar-layout.component'; 4 | 5 | describe('SidbarLayoutComponent', () => { 6 | let component: SidebarLayoutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SidebarLayoutComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SidebarLayoutComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/sidebar-layout/sidebar-layout.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {AuthenticationService} from '../../services/authentication.service'; 3 | import {EventType, Router} from '@angular/router'; 4 | 5 | export type SidebarLayoutEntryModel = { 6 | title: string, 7 | icon: string, 8 | path: string, 9 | selected?: boolean 10 | } 11 | 12 | export type SidebarLayoutModel = { 13 | entries: SidebarLayoutEntryModel[], 14 | } 15 | 16 | @Component({ 17 | selector: 'app-sidbar-layout', 18 | templateUrl: './sidebar-layout.component.html', 19 | styleUrls: ['./sidebar-layout.component.scss'] 20 | }) 21 | export class SidebarLayoutComponent implements OnInit { 22 | 23 | @Input() 24 | model: SidebarLayoutModel | undefined; 25 | window: any; 26 | 27 | constructor(public auth: AuthenticationService, private router: Router) { 28 | this.window = window; 29 | } 30 | 31 | ngOnInit(): void { 32 | this.router.events.subscribe(e => { 33 | if ((e as any).type === EventType.NavigationEnd) { 34 | const entry = this.model?.entries.find(entry => (e as any).url.toLowerCase().startsWith(entry.path)); 35 | if (entry) { 36 | this.selectMenuEntry(entry); 37 | } 38 | } 39 | 40 | }) 41 | } 42 | 43 | selectMenuEntry(entry: SidebarLayoutEntryModel) { 44 | this.model?.entries.forEach(e => { 45 | if (e.path === entry.path) { 46 | e.selected = true; 47 | } else { 48 | e.selected = false; 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/sidebar-layout/sidebar-layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SidebarLayoutComponent } from './sidebar-layout.component'; 4 | import {MatIconModule} from '@angular/material/icon'; 5 | import {RouterLinkWithHref} from '@angular/router'; 6 | 7 | 8 | 9 | @NgModule({ 10 | declarations: [ 11 | SidebarLayoutComponent 12 | ], 13 | exports: [ 14 | SidebarLayoutComponent, 15 | 16 | ], 17 | imports: [ 18 | CommonModule, 19 | MatIconModule, 20 | RouterLinkWithHref 21 | ] 22 | }) 23 | export class SidebarLayoutModule { } 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/terminal/terminal.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/terminal/terminal.component.scss: -------------------------------------------------------------------------------- 1 | .outer-box{ 2 | min-height: 40vh; 3 | /* TODO: setting max-height doesn't work with ng-terminal's fit() */ 4 | /* TODO: automatically resizing following an outer box */ 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/terminal/terminal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TerminalComponent } from './terminal.component'; 4 | 5 | describe('TerminalComponent', () => { 6 | let component: TerminalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TerminalComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TerminalComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/ui/terminal/terminal.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {TerminalComponent} from './terminal.component'; 4 | import {NgTerminalModule} from 'ng-terminal'; 5 | 6 | 7 | @NgModule({ 8 | declarations: [ 9 | TerminalComponent 10 | ], 11 | imports: [ 12 | CommonModule, 13 | NgTerminalModule 14 | ], 15 | exports: [TerminalComponent] 16 | }) 17 | export class TerminalModule { 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/images/admin.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/assets/images/admin.PNG -------------------------------------------------------------------------------- /ui/src/assets/images/openttd-server-logo.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/assets/images/openttd-server-logo.PNG -------------------------------------------------------------------------------- /ui/src/assets/images/server-logo-default.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/assets/images/server-logo-default.PNG -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | const l = location 2 | 3 | export const environment = { 4 | production: true, 5 | baseUrl: l.protocol + '//' + l.host, 6 | wsServerRoot: 'ws://' + l.host 7 | }; 8 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | const devPort = 8080 5 | const l = location 6 | 7 | export const environment = { 8 | production: false, 9 | baseUrl: l.protocol + '//' + l.hostname + ':' + devPort, 10 | wsServerRoot: 'ws://' + l.hostname + ':' + devPort 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/plugins/zone-error'; // Included with Angular CLI. 21 | -------------------------------------------------------------------------------- /ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreashauschild/openttd-server/9d94ce6136e3b2aa80d6989cd0bf39e0776db597/ui/src/favicon.ico -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenTTD Server 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/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().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /ui/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | @include mat.core(); 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | html, body { height: 100%; } 9 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 10 | 11 | 12 | .menu-selected { 13 | @apply bg-gray-900 text-white flex items-center px-2 py-2 text-sm font-medium rounded-md; 14 | } 15 | 16 | .menu-unselected { 17 | @apply text-gray-300 hover:bg-gray-700 hover:text-white flex items-center px-2 py-2 text-sm font-medium rounded-md; 18 | } 19 | 20 | .snackbar{ 21 | background: white; 22 | border-radius: 4px; 23 | box-sizing: border-box; 24 | display: block; 25 | margin: 0px !important; 26 | max-width: 1200vw; 27 | min-width: 800px !important; 28 | padding: 0px !important; 29 | min-height: 0px !important; 30 | transform-origin: center; 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().forEach(context); 27 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{html,ts}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [ 10 | require('@tailwindcss/forms'), 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /ui/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": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui/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 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/tsconfig.spec.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/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ui/update-api.ps1: -------------------------------------------------------------------------------- 1 | # Script thats reflects the curren quarkus api to the angular frontend 2 | cd "$PSScriptRoot" 3 | $url = "http://localhost:8080/q/openapi" 4 | $output = "openapi\service.yml" 5 | $start_time = Get-Date 6 | 7 | Invoke-WebRequest -Uri $url -OutFile $output 8 | Write-Output "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" 9 | node_modules\.bin\ng-openapi-gen --input openapi/service.yml --output src/app/api --ignoreUnusedModels=false 10 | 11 | 12 | # Replace Root ur 13 | $file='src\app\api\api-configuration.ts' 14 | $regex = "rootUrl: string = '';" 15 | (Get-Content $file) -replace $regex, "rootUrl: string = environment.baseUrl;" | Set-Content $file 16 | 17 | # Add Import as first line 18 | $content = Get-Content -Path $file 19 | $Output = @() 20 | $Output += "import {environment} from '../../environments/environment';" 21 | $Output += $content 22 | $Output | Out-file -encoding UTF8 $file 23 | -------------------------------------------------------------------------------- /ui/update-api.sh: -------------------------------------------------------------------------------- 1 | # Script thats reflects the curren quarkus api to the angular frontend 2 | 3 | cd $(dirname "$0") 4 | 5 | url="http://localhost:8080/q/openapi" 6 | output="openapi/service.yml" 7 | 8 | wget -O "${output}" "${url}" 9 | node_modules/.bin/ng-openapi-gen --input openapi/service.yml --output src/app/api --ignoreUnusedModels=false 10 | # 11 | # 12 | ## Replace Root ur 13 | file='src/app/api/api-configuration.ts' 14 | regex="rootUrl: string = '';" 15 | sed -i -E "s/${regex}/rootUrl: string = environment.baseUrl;/" "${file}" 16 | 17 | ## Add Import as first line 18 | echo "import {environment} from '../../environments/environment';$(cat "${file}")" > "${file}" 19 | --------------------------------------------------------------------------------