Merge branch 'main' into patterned-territory
@@ -12,4 +12,4 @@ helm-charts
|
||||
.env
|
||||
.editorconfig
|
||||
.idea
|
||||
coverage*
|
||||
coverage*
|
||||
|
||||
@@ -7,3 +7,6 @@ end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
|
||||
[*.sh]
|
||||
indent_size = 4
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: "📝 API/DB Feature Request"
|
||||
about: Suggest a new backend API or database feature
|
||||
title: "[Feature] <short description here>"
|
||||
labels: [feature, backend]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## ✨ API/Database Feature Request
|
||||
|
||||
### Summary
|
||||
|
||||
Describe the feature being requested. What functionality does it add to the backend or database? What problem does it solve?
|
||||
|
||||
## 📘 Use Case
|
||||
|
||||
Explain the use case behind this feature. Who is it for, and why is it needed now?
|
||||
|
||||
## 📥 Example API Request
|
||||
|
||||
```
|
||||
POST /api/example-endpoint
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
|
||||
{
|
||||
"exampleField": "value",
|
||||
"anotherField": 123
|
||||
}
|
||||
```
|
||||
|
||||
## 📤 Example API Response
|
||||
|
||||
```
|
||||
// JSON response
|
||||
{
|
||||
"id": "abc123",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"result": true,
|
||||
"timestamp": "2025-04-30T18:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 Database Considerations
|
||||
|
||||
- **New Tables**: Yes/No
|
||||
If yes, describe the schema or include a rough layout.
|
||||
|
||||
- **Modified Tables**: Yes/No
|
||||
Describe what changes are needed and why.
|
||||
|
||||
- **Migrations Required**: Yes/No
|
||||
|
||||
- **Indexes Required**: Yes/No
|
||||
Include any thoughts on performance or query efficiency.
|
||||
|
||||
## 🔐 Security & Access Control
|
||||
|
||||
- Does the endpoint require authentication? Yes/No
|
||||
- Should it be limited to specific roles (e.g. admin, worker, user)?
|
||||
- Any sensitive data in the request or response?
|
||||
|
||||
## 📎 Additional Context
|
||||
|
||||
Add any screenshots, designs, relevant discussion links, or references to prior issues.
|
||||
@@ -1,7 +1,11 @@
|
||||
name: CI
|
||||
name: 🧪 CI
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -12,13 +16,23 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup npm
|
||||
run: npm install
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- name: Build
|
||||
run: npm run build-prod
|
||||
- run: npm ci
|
||||
- run: npm run build-prod
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: out/index.html
|
||||
retention-days: 1
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
@@ -1,77 +1,179 @@
|
||||
name: 🚀 Deploy
|
||||
|
||||
on:
|
||||
# Allow contributors to schedule manual deployments.
|
||||
# Permission to deploy can be restricted by requiring approval in environment configuration.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_environment:
|
||||
description: "Deployment Environment"
|
||||
target_domain:
|
||||
description: "Deployment Domain"
|
||||
required: true
|
||||
default: "staging"
|
||||
default: "openfront.dev"
|
||||
type: choice
|
||||
options:
|
||||
- prod
|
||||
- staging
|
||||
- openfront.io
|
||||
- openfront.dev
|
||||
target_host:
|
||||
description: "Deployment Host"
|
||||
required: true
|
||||
default: "staging"
|
||||
type: choice
|
||||
options:
|
||||
- eu
|
||||
- us
|
||||
- masters
|
||||
- nbg1
|
||||
- staging
|
||||
target_subdomain:
|
||||
description: "Deployment Subdomain"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
# Automatic deployment on push
|
||||
# See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "*"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# Don't deploy on push if this is a fork
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.repository == 'openfrontio/OpenFrontIO' }}
|
||||
# Use different logic based on event type
|
||||
name: Deploy to ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}
|
||||
name: ${{
|
||||
github.event_name == 'push'
|
||||
&& (github.ref_name == 'main' && 'openfront.dev'
|
||||
|| format('{0}.openfront.dev', github.ref_name))
|
||||
|| inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain)
|
||||
|| inputs.target_domain
|
||||
|| 'openfront.dev'
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}
|
||||
|
||||
environment: ${{ inputs.target_domain == 'openfront.io' && 'prod' || '' }}
|
||||
env:
|
||||
DOMAIN: ${{ inputs.target_domain || 'openfront.dev' }}
|
||||
SUBDOMAIN: ${{ github.event_name == 'push' && github.ref_name || inputs.target_subdomain || 'main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Log in to Docker Hub
|
||||
- name: 📝 Update job summary
|
||||
env:
|
||||
FQDN: ${{ env.SUBDOMAIN && format('{0}.{1}', env.SUBDOMAIN, env.DOMAIN) || env.DOMAIN || 'openfront.dev' }}
|
||||
run: |
|
||||
echo "FQDN=$FQDN" >> $GITHUB_ENV
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### In progress :ship:
|
||||
|
||||
Deploying from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: generate-token
|
||||
if: ${{ github.repository == 'openfrontio/OpenFrontIO' }}
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- name: Export the token
|
||||
if: ${{ github.repository == 'openfrontio/OpenFrontIO' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
run: |
|
||||
echo "GH_TOKEN=$GH_TOKEN" >> $GITHUB_ENV
|
||||
gh api octocat
|
||||
- name: 📝 Create deployment
|
||||
uses: chrnorm/deployment-action@v2
|
||||
id: deployment
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
environment-url: https://${{ env.FQDN }}
|
||||
environment: ${{ env.FQDN }}
|
||||
- name: 🔗 Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- run: |
|
||||
- name: 🔑 Create SSH private key
|
||||
env:
|
||||
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
||||
SERVER_HOST_NBG1: ${{ secrets.SERVER_HOST_NBG1 }}
|
||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
|
||||
test -n "$SERVER_HOST_MASTERS" && ssh-keyscan -H "$SERVER_HOST_MASTERS" >> ~/.ssh/known_hosts
|
||||
test -n "$SERVER_HOST_NBG1" && ssh-keyscan -H "$SERVER_HOST_NBG1" >> ~/.ssh/known_hosts
|
||||
test -n "$SERVER_HOST_STAGING" && ssh-keyscan -H "$SERVER_HOST_STAGING" >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_HOST_STAGING }} >> ~/.ssh/known_hosts
|
||||
- name: 🚢 Deploy
|
||||
env:
|
||||
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
|
||||
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
|
||||
HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}
|
||||
MON_PASSWORD: ${{ secrets.MON_PASSWORD }}
|
||||
MON_USERNAME: ${{ secrets.MON_USERNAME }}
|
||||
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
||||
SERVER_HOST_NBG1: ${{ secrets.SERVER_HOST_NBG1 }}
|
||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
VERSION_TAG: latest
|
||||
run: |
|
||||
echo "::group::deploy.sh"
|
||||
./deploy.sh "$ENV" "$HOST" "$SUBDOMAIN"
|
||||
echo "Deployment created in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY
|
||||
echo "::endgroup::"
|
||||
- name: ⏳ Wait for deployment to start
|
||||
run: |
|
||||
echo "::group::Wait for deployment to start"
|
||||
set -euxo pipefail
|
||||
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
|
||||
if [ "$SECONDS" -ge 300 ]; then
|
||||
echo "Timeout: deployment did not start within 5 minutes"
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "Deployment started in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY
|
||||
echo "::endgroup::"
|
||||
- name: ✅ Update deployment status
|
||||
if: success()
|
||||
uses: chrnorm/deployment-status@v2
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
environment-url: https://${{ env.FQDN }}
|
||||
state: success
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
- name: ✅ Update job summary
|
||||
if: success()
|
||||
run: |
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### Success! :rocket:
|
||||
|
||||
# Determine environment based on trigger type
|
||||
TARGET_ENV="${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}"
|
||||
TARGET_HOST="${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}"
|
||||
TARGET_SUBDOMAIN="${{ github.event_name == 'workflow_dispatch' && inputs.target_subdomain || 'main' }}"
|
||||
|
||||
cat >.env.$TARGET_ENV <<EOF
|
||||
ADMIN_TOKEN=${{ secrets.ADMIN_TOKEN }}
|
||||
CF_ACCOUNT_ID=${{ secrets.CF_ACCOUNT_ID }}
|
||||
CF_API_TOKEN=${{ secrets.CF_API_TOKEN }}
|
||||
DOCKER_REPO=${{ vars.DOCKERHUB_REPO }}
|
||||
DOCKER_USERNAME=${{ vars.DOCKERHUB_USERNAME }}
|
||||
DOMAIN=${{ vars.DOMAIN }}
|
||||
MON_PASSWORD=${{ secrets.MON_PASSWORD }}
|
||||
MON_USERNAME=${{ secrets.MON_USERNAME }}
|
||||
R2_ACCESS_KEY=${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET=${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY=${{ secrets.R2_SECRET_KEY }}
|
||||
SERVER_HOST_STAGING=${{ secrets.SERVER_HOST_STAGING }}
|
||||
SERVER_HOST_US=${{ secrets.SERVER_HOST_US }}
|
||||
SERVER_HOST_EU=${{ secrets.SERVER_HOST_EU }}
|
||||
SSH_KEY=~/.ssh/id_rsa
|
||||
VERSION_TAG="latest"
|
||||
Deployed from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
- name: ❌ Update deployment status
|
||||
if: failure()
|
||||
uses: chrnorm/deployment-status@v2
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
environment-url: https://${{ env.FQDN }}
|
||||
state: failure
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
- name: ❌ Update job summary
|
||||
if: failure()
|
||||
run: |
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### Failure! :fire:
|
||||
|
||||
./deploy.sh $TARGET_ENV $TARGET_HOST $TARGET_SUBDOMAIN
|
||||
echo "Deployed to $TARGET_ENV environment on $TARGET_HOST host with subdomain $TARGET_SUBDOMAIN"
|
||||
Unable to deploy from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
name: ESLint Check
|
||||
name: 🔍 ESLint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: 🧼 PR Description
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
validate-description:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.pull_request.body || '';
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Check for ## Description section
|
||||
const descMatch = body.match(/^## Description:\s*\n((?:(?!^#).*\n?)*)/m);
|
||||
if (!descMatch || descMatch[1].trim().length < 20) {
|
||||
errors.push('❌ Missing or short `## Description:` section.');
|
||||
}
|
||||
|
||||
// Check all three boxes are checked
|
||||
const requiredBoxes = [
|
||||
/- \[x\] I have added screenshots for all UI updates/i,
|
||||
/- \[x\] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced/i,
|
||||
/- \[x\] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors/i
|
||||
];
|
||||
|
||||
for (const box of requiredBoxes) {
|
||||
if (!box.test(body)) {
|
||||
errors.push('❌ One or more checklist items are not checked.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
core.setFailed(errors.join('\n'));
|
||||
} else {
|
||||
console.log('✅ PR description and checklist look good.');
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
name: Prettier Check
|
||||
name: 🎨 Prettier
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -6,4 +6,4 @@ TODO.txt
|
||||
resources/images/.DS_Store
|
||||
resources/.DS_Store
|
||||
.env*
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
|
||||
|
||||
# Then run lint-staged if tests pass
|
||||
npx lint-staged
|
||||
npx lint-staged
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-organize-imports"]
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".husky/**",
|
||||
"options": {
|
||||
"plugins": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "Dockerfile",
|
||||
"options": {
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-sh"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,25 @@
|
||||
"runtimeArgs": ["run-script", "test"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Server",
|
||||
"runtimeExecutable": "node",
|
||||
"runtimeArgs": [
|
||||
"--loader",
|
||||
"ts-node/esm",
|
||||
"--experimental-specifier-resolution=node",
|
||||
"${workspaceFolder}/src/server/Server.ts"
|
||||
],
|
||||
"env": {
|
||||
"GAME_ENV": "dev"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"sourceMaps": true,
|
||||
"restart": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
* @openfrontio/core-contributor
|
||||
resources/lang @openfrontio/translation-approver
|
||||
resources/lang/en.json
|
||||
@@ -1,9 +1,8 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
FROM node:18 AS base
|
||||
|
||||
# Install Nginx, Supervisor, Git, jq, curl, and Node Exporter dependencies
|
||||
# Create dependency layer
|
||||
FROM base AS dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
supervisor \
|
||||
@@ -11,19 +10,22 @@ RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
jq \
|
||||
wget \
|
||||
apache2-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node Exporter
|
||||
RUN mkdir -p /opt/node_exporter && \
|
||||
wget -qO- https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz | \
|
||||
tar xvz --strip-components=1 -C /opt/node_exporter && \
|
||||
ln -s /opt/node_exporter/node_exporter /usr/local/bin/
|
||||
|
||||
# Install cloudflared
|
||||
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
|
||||
&& dpkg -i cloudflared.deb \
|
||||
&& rm cloudflared.deb
|
||||
|
||||
# Final image
|
||||
FROM base
|
||||
|
||||
# Copy installed packages from dependencies stage
|
||||
COPY --from=dependencies / /
|
||||
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -31,7 +33,7 @@ WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies while bypassing Husky hooks
|
||||
ENV HUSKY=0
|
||||
ENV HUSKY=0
|
||||
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
|
||||
RUN mkdir -p .git && npm install
|
||||
|
||||
@@ -41,6 +43,10 @@ COPY . .
|
||||
# Build the client-side application
|
||||
RUN npm run build-prod
|
||||
|
||||
# So we can see which commit was used to build the container
|
||||
# https://openfront.io/commit.txt
|
||||
RUN echo $GIT_COMMIT > static/commit.txt
|
||||
|
||||
# Copy Nginx configuration and ensure it's used instead of the default
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -f /etc/nginx/sites-enabled/default
|
||||
@@ -54,4 +60,4 @@ COPY startup.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/startup.sh
|
||||
|
||||
# Use the startup script as the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/startup.sh"]
|
||||
ENTRYPOINT ["/usr/local/bin/startup.sh"]
|
||||
|
||||
@@ -116,21 +116,23 @@ This project is licensed under the terms found in the [LICENSE](LICENSE) file.
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Request to join the development [Discord](https://discord.gg/K9zernJB5z).
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
1. Create your feature branch (`git checkout -b amazing-feature`)
|
||||
1. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
1. Push to the branch (`git push origin amazing-feature`)
|
||||
1. Open a Pull Request
|
||||
|
||||
## 🌐 Translation
|
||||
|
||||
Translators are welcome! Please feel free to help translate into your language.
|
||||
How to help?
|
||||
|
||||
1. Request to join the translation [Discord](https://discord.gg/rUukAnz4Ww)
|
||||
1. Go to the project's Crowdin translation page: [https://crowdin.com/project/openfront-mls](https://crowdin.com/project/openfront-mls)
|
||||
2. Login if you already have an account/ Sign up if you don't have one
|
||||
3. Select the language you want to translate in/ If your language isn't on the list, click the "Request New Language" button and enter the language you want added there.
|
||||
4. Translate the strings
|
||||
1. Login if you already have an account/ Sign up if you don't have one
|
||||
1. Select the language you want to translate in/ If your language isn't on the list, click the "Request New Language" button and enter the language you want added there.
|
||||
1. Translate the strings
|
||||
|
||||
### Project Governance
|
||||
|
||||
|
||||
@@ -5,26 +5,47 @@
|
||||
# 2. Copies the update script to Hetzner server
|
||||
# 3. Executes the update script on the Hetzner server
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
# Initialize variables
|
||||
ENABLE_BASIC_AUTH=false
|
||||
|
||||
# Parse command line arguments
|
||||
POSITIONAL_ARGS=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--enable_basic_auth)
|
||||
ENABLE_BASIC_AUTH=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Restore positional parameters
|
||||
set -- "${POSITIONAL_ARGS[@]}"
|
||||
|
||||
# Check command line arguments
|
||||
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
||||
echo "Error: Please specify environment and host, with optional subdomain"
|
||||
echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]"
|
||||
echo "Usage: $0 [prod|staging] [eu|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate first argument (environment)
|
||||
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
|
||||
echo "Error: First argument must be either 'prod' or 'staging'"
|
||||
echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]"
|
||||
echo "Usage: $0 [prod|staging] [eu|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate second argument (host)
|
||||
if [ "$2" != "eu" ] && [ "$2" != "us" ] && [ "$2" != "staging" ]; then
|
||||
echo "Error: Second argument must be either 'eu', 'us', or 'staging'"
|
||||
echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]"
|
||||
if [ "$2" != "eu" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
|
||||
echo "Error: Second argument must be either 'eu', 'nbg1', 'staging', or 'masters'"
|
||||
echo "Usage: $0 [prod|staging] [eu|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -37,7 +58,7 @@ print_header() {
|
||||
|
||||
ENV=$1
|
||||
HOST=$2
|
||||
SUBDOMAIN=$3 # Optional third argument for custom subdomain
|
||||
SUBDOMAIN=$3 # Optional third argument for custom subdomain
|
||||
|
||||
# Set subdomain - use the custom subdomain if provided, otherwise use REGION
|
||||
if [ -n "$SUBDOMAIN" ]; then
|
||||
@@ -57,17 +78,17 @@ fi
|
||||
if [ -f .env.$ENV ]; then
|
||||
echo "Loading $ENV-specific configuration from .env.$ENV file..."
|
||||
export $(grep -v '^#' .env.$ENV | xargs)
|
||||
else
|
||||
echo "Error: Environment file .env.$ENV not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$HOST" == "staging" ]; then
|
||||
print_header "DEPLOYING TO STAGING HOST"
|
||||
SERVER_HOST=$SERVER_HOST_STAGING
|
||||
elif [ "$HOST" == "us" ]; then
|
||||
print_header "DEPLOYING TO US HOST"
|
||||
SERVER_HOST=$SERVER_HOST_US
|
||||
elif [ "$HOST" == "nbg1" ]; then
|
||||
print_header "DEPLOYING TO NBG1 HOST"
|
||||
SERVER_HOST=$SERVER_HOST_NBG1
|
||||
elif [ "$HOST" == "masters" ]; then
|
||||
print_header "DEPLOYING TO MASTERS HOST"
|
||||
SERVER_HOST=$SERVER_HOST_MASTERS
|
||||
else
|
||||
print_header "DEPLOYING TO EU HOST"
|
||||
SERVER_HOST=$SERVER_HOST_EU
|
||||
@@ -79,14 +100,29 @@ if [ -z "$SERVER_HOST" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
UPDATE_SCRIPT="./update.sh" # Path to your update script
|
||||
REMOTE_USER="openfront"
|
||||
REMOTE_UPDATE_PATH="/home/$REMOTE_USER"
|
||||
REMOTE_UPDATE_SCRIPT="$REMOTE_UPDATE_PATH/update-openfront.sh" # Where to place the script on server
|
||||
# Check if basic auth is enabled and credentials are available
|
||||
if [ "$ENABLE_BASIC_AUTH" = true ]; then
|
||||
print_header "BASIC AUTH ENABLED"
|
||||
if [ -z "$BASIC_AUTH_USER" ] || [ -z "$BASIC_AUTH_PASS" ]; then
|
||||
echo "Error: Basic Auth is enabled but BASIC_AUTH_USER or BASIC_AUTH_PASS not defined in .env file or environment"
|
||||
exit 1
|
||||
fi
|
||||
echo "Basic Authentication will be enabled with user: $BASIC_AUTH_USER"
|
||||
else
|
||||
# If basic auth is not enabled, set the variables to empty to ensure they don't get used
|
||||
BASIC_AUTH_USER=""
|
||||
BASIC_AUTH_PASS=""
|
||||
echo "Basic Authentication is disabled"
|
||||
fi
|
||||
|
||||
IMAGE_NAME="${DOCKER_USERNAME}/${DOCKER_REPO}"
|
||||
DOCKER_IMAGE="${IMAGE_NAME}:${VERSION_TAG}"
|
||||
# Configuration
|
||||
UPDATE_SCRIPT="./update.sh" # Path to your update script
|
||||
REMOTE_USER="openfront"
|
||||
REMOTE_UPDATE_PATH="/home/$REMOTE_USER"
|
||||
REMOTE_UPDATE_SCRIPT="$REMOTE_UPDATE_PATH/update-openfront.sh" # Where to place the script on server
|
||||
|
||||
VERSION_TAG=$(date +"%Y%m%d-%H%M%S")
|
||||
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${VERSION_TAG}"
|
||||
|
||||
# Check if update script exists
|
||||
if [ ! -f "$UPDATE_SCRIPT" ]; then
|
||||
@@ -103,15 +139,15 @@ echo "Using version tag: $VERSION_TAG"
|
||||
echo "Docker repository: $DOCKER_REPO"
|
||||
|
||||
# Get Git commit for build info
|
||||
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
GIT_COMMIT=$(git rev-parse HEAD 2> /dev/null || echo "unknown")
|
||||
echo "Git commit: $GIT_COMMIT"
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg GIT_COMMIT=$GIT_COMMIT \
|
||||
-t $DOCKER_USERNAME/$DOCKER_REPO:$VERSION_TAG \
|
||||
--push \
|
||||
.
|
||||
--platform linux/amd64 \
|
||||
--build-arg GIT_COMMIT=$GIT_COMMIT \
|
||||
-t $DOCKER_IMAGE \
|
||||
--push \
|
||||
.
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Docker build failed. Stopping deployment."
|
||||
@@ -140,7 +176,6 @@ cat > $REMOTE_UPDATE_PATH/.env << 'EOL'
|
||||
GAME_ENV=$ENV
|
||||
ENV=$ENV
|
||||
HOST=$HOST
|
||||
SUBDOMAIN=$SUBDOMAIN
|
||||
DOCKER_IMAGE=$DOCKER_IMAGE
|
||||
DOCKER_TOKEN=$DOCKER_TOKEN
|
||||
ADMIN_TOKEN=$ADMIN_TOKEN
|
||||
@@ -151,8 +186,11 @@ R2_BUCKET=$R2_BUCKET
|
||||
CF_API_TOKEN=$CF_API_TOKEN
|
||||
DOMAIN=$DOMAIN
|
||||
SUBDOMAIN=$SUBDOMAIN
|
||||
MON_USERNAME=$MON_USERNAME
|
||||
MON_PASSWORD=$MON_PASSWORD
|
||||
OTEL_USERNAME=$OTEL_USERNAME
|
||||
OTEL_PASSWORD=$OTEL_PASSWORD
|
||||
OTEL_ENDPOINT=$OTEL_ENDPOINT
|
||||
BASIC_AUTH_USER=$BASIC_AUTH_USER
|
||||
BASIC_AUTH_PASS=$BASIC_AUTH_PASS
|
||||
EOL
|
||||
chmod 600 $REMOTE_UPDATE_PATH/.env && \
|
||||
$REMOTE_UPDATE_SCRIPT"
|
||||
@@ -164,5 +202,8 @@ fi
|
||||
|
||||
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
|
||||
echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!"
|
||||
if [ "$ENABLE_BASIC_AUTH" = true ]; then
|
||||
echo "🔒 Basic authentication enabled with user: $BASIC_AUTH_USER"
|
||||
fi
|
||||
echo "🌐 Check your server to verify the deployment."
|
||||
echo "======================================================="
|
||||
echo "======================================================="
|
||||
|
||||
@@ -31,4 +31,10 @@ export default [
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// Enable rules
|
||||
eqeqeq: "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,4 +29,4 @@ MON_USERNAME=monitor_username
|
||||
MON_PASSWORD=monitor_password
|
||||
|
||||
# Version
|
||||
VERSION_TAG="latest"
|
||||
VERSION_TAG="latest"
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Metric Collector for Prometheus Pushgateway
|
||||
# This script collects metrics from Node Exporter and application sources
|
||||
# and pushes them to a Prometheus Pushgateway with custom labels.
|
||||
|
||||
# Configuration
|
||||
NODE_EXPORTER_URL="http://localhost:9100/metrics"
|
||||
APP_METRICS_URL="http://localhost:9090/metrics"
|
||||
PUSHGATEWAY_BASE_URL="https://mon.openfront.io/pushgateway/metrics"
|
||||
AUTH=$MON_USERNAME:$MON_PASSWORD
|
||||
INTERVAL=15 # seconds
|
||||
|
||||
# Function to fetch metrics from Node Exporter
|
||||
fetch_node_exporter_metrics() {
|
||||
curl -s --connect-timeout 5 --max-time 10 "$NODE_EXPORTER_URL" ||
|
||||
echo "# Error fetching Node Exporter metrics"
|
||||
}
|
||||
|
||||
# Function to fetch metrics from your application
|
||||
fetch_app_metrics() {
|
||||
curl -s --connect-timeout 5 --max-time 10 "$APP_METRICS_URL" ||
|
||||
echo "# Error fetching application metrics"
|
||||
}
|
||||
|
||||
# Function to push metrics to Pushgateway
|
||||
push_metrics() {
|
||||
local metrics=$1
|
||||
local job_name=$2
|
||||
|
||||
echo "Pushing $job_name metrics to Pushgateway..."
|
||||
|
||||
# Create a temporary file for the metrics
|
||||
TEMP_FILE=$(mktemp)
|
||||
echo "$metrics" > "$TEMP_FILE"
|
||||
|
||||
# Push to Pushgateway with instance label
|
||||
curl -s -u "$AUTH" --data-binary @"$TEMP_FILE" \
|
||||
"$PUSHGATEWAY_BASE_URL/job/$job_name/instance/$HOST"
|
||||
|
||||
# Check if push was successful
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$job_name metrics pushed successfully"
|
||||
else
|
||||
echo "Error pushing $job_name metrics"
|
||||
fi
|
||||
|
||||
# Remove temporary file
|
||||
rm "$TEMP_FILE"
|
||||
}
|
||||
|
||||
# Function to add labels to metrics
|
||||
add_labels() {
|
||||
local metrics=$1
|
||||
|
||||
# First, handle metrics with existing labels
|
||||
metrics=$(echo "$metrics" | sed -E 's/(\{[^}]*)\}/\1,env="'$ENV'",host="'$HOST'",subdomain="'$SUBDOMAIN'"}/g')
|
||||
|
||||
# Then, handle metrics with no existing labels
|
||||
metrics=$(echo "$metrics" | sed -E 's/^([a-zA-Z0-9_:]+)[ \t]+([0-9.e+-]+)$/\1{env="'$ENV'",host="'$HOST'",subdomain="'$SUBDOMAIN'"} \2/g')
|
||||
|
||||
echo "$metrics"
|
||||
}
|
||||
|
||||
# Main function to collect and push metrics
|
||||
collect_and_push_metrics() {
|
||||
echo "Starting metrics collection cycle at $(date)"
|
||||
|
||||
# Get metrics from both sources
|
||||
NODE_METRICS=$(fetch_node_exporter_metrics)
|
||||
APP_METRICS=$(fetch_app_metrics)
|
||||
|
||||
# Clean up metrics (remove headers etc.)
|
||||
NODE_METRICS=$(echo "$NODE_METRICS" | grep -v "^Fetching")
|
||||
APP_METRICS=$(echo "$APP_METRICS" | grep -v "^Fetching")
|
||||
|
||||
# Add labels to metrics
|
||||
NODE_METRICS=$(add_labels "$NODE_METRICS")
|
||||
APP_METRICS=$(add_labels "$APP_METRICS")
|
||||
|
||||
# Push to Pushgateway separately
|
||||
push_metrics "$NODE_METRICS" "node_exporter"
|
||||
push_metrics "$APP_METRICS" "app_metrics"
|
||||
|
||||
echo "Metrics collection cycle completed at $(date)"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
echo "===== Starting metrics collector ====="
|
||||
echo "Environment: $ENV, HOST: $HOST, Subdomain: $SUBDOMAIN"
|
||||
echo "Collecting and pushing metrics every $INTERVAL seconds"
|
||||
echo "Node Exporter URL: $NODE_EXPORTER_URL"
|
||||
echo "App Metrics URL: $APP_METRICS_URL"
|
||||
echo "Pushgateway URL: $PUSHGATEWAY_BASE_URL"
|
||||
|
||||
# Wait for app to be ready.
|
||||
sleep 30
|
||||
|
||||
# Then set up interval loop
|
||||
while true; do
|
||||
sleep $INTERVAL
|
||||
collect_and_push_metrics
|
||||
done
|
||||
@@ -15,6 +15,32 @@ map $uri $port {
|
||||
~^/w12/ 3013;
|
||||
~^/w13/ 3014;
|
||||
~^/w14/ 3015;
|
||||
~^/w15/ 3016;
|
||||
~^/w16/ 3017;
|
||||
~^/w17/ 3018;
|
||||
~^/w18/ 3019;
|
||||
~^/w19/ 3020;
|
||||
~^/w20/ 3021;
|
||||
~^/w21/ 3022;
|
||||
~^/w22/ 3023;
|
||||
~^/w23/ 3024;
|
||||
~^/w24/ 3025;
|
||||
~^/w25/ 3026;
|
||||
~^/w26/ 3027;
|
||||
~^/w27/ 3028;
|
||||
~^/w28/ 3029;
|
||||
~^/w29/ 3030;
|
||||
~^/w30/ 3031;
|
||||
~^/w31/ 3032;
|
||||
~^/w32/ 3033;
|
||||
~^/w33/ 3034;
|
||||
~^/w34/ 3035;
|
||||
~^/w35/ 3036;
|
||||
~^/w36/ 3037;
|
||||
~^/w37/ 3038;
|
||||
~^/w38/ 3039;
|
||||
~^/w39/ 3040;
|
||||
~^/w40/ 3041;
|
||||
default 3000;
|
||||
}
|
||||
|
||||
@@ -235,6 +261,32 @@ server {
|
||||
if ($worker = "12") { set $worker_port 3013; }
|
||||
if ($worker = "13") { set $worker_port 3014; }
|
||||
if ($worker = "14") { set $worker_port 3015; }
|
||||
if ($worker = "15") { set $worker_port 3016; }
|
||||
if ($worker = "16") { set $worker_port 3017; }
|
||||
if ($worker = "17") { set $worker_port 3018; }
|
||||
if ($worker = "18") { set $worker_port 3019; }
|
||||
if ($worker = "19") { set $worker_port 3020; }
|
||||
if ($worker = "20") { set $worker_port 3021; }
|
||||
if ($worker = "21") { set $worker_port 3022; }
|
||||
if ($worker = "22") { set $worker_port 3023; }
|
||||
if ($worker = "23") { set $worker_port 3024; }
|
||||
if ($worker = "24") { set $worker_port 3025; }
|
||||
if ($worker = "25") { set $worker_port 3026; }
|
||||
if ($worker = "26") { set $worker_port 3027; }
|
||||
if ($worker = "27") { set $worker_port 3028; }
|
||||
if ($worker = "28") { set $worker_port 3029; }
|
||||
if ($worker = "29") { set $worker_port 3030; }
|
||||
if ($worker = "30") { set $worker_port 3031; }
|
||||
if ($worker = "31") { set $worker_port 3032; }
|
||||
if ($worker = "32") { set $worker_port 3033; }
|
||||
if ($worker = "33") { set $worker_port 3034; }
|
||||
if ($worker = "34") { set $worker_port 3035; }
|
||||
if ($worker = "35") { set $worker_port 3036; }
|
||||
if ($worker = "36") { set $worker_port 3037; }
|
||||
if ($worker = "37") { set $worker_port 3038; }
|
||||
if ($worker = "38") { set $worker_port 3039; }
|
||||
if ($worker = "39") { set $worker_port 3040; }
|
||||
if ($worker = "40") { set $worker_port 3041; }
|
||||
|
||||
proxy_pass http://127.0.0.1:$worker_port$2;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-sh": "^0.17.4",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sinon": "^18.0.0",
|
||||
"sinon-chai": "^4.0.0",
|
||||
@@ -80,6 +81,18 @@
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/secret-manager": "^5.6.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.200.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.58.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/host-metrics": "^0.36.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-logs": "^0.200.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.200.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.32.0",
|
||||
"@opentelemetry/winston-transport": "^0.11.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
@@ -95,7 +108,7 @@
|
||||
"d3": "^7.9.0",
|
||||
"discord.js": "^14.16.3",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"google-auth-library": "^9.14.0",
|
||||
@@ -104,13 +117,14 @@
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"jimp": "^0.22.12",
|
||||
"jose": "^6.0.10",
|
||||
"lit": "^3.2.1",
|
||||
"msgpack5": "^6.0.2",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-addon-api": "^8.1.0",
|
||||
"node-gyp": "^10.2.0",
|
||||
"obscenity": "^0.4.3",
|
||||
"page": "^1.11.6",
|
||||
"page": "^1.3.7",
|
||||
"pg": "^8.13.3",
|
||||
"priority-queue-typescript": "^1.0.1",
|
||||
"prom-client": "^15.1.3",
|
||||
@@ -127,6 +141,7 @@
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"wheelnav": "^1.7.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston-transport": "^4.9.0",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"help": [
|
||||
{
|
||||
"key": "troops",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "gold",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "no_attack",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "sorry_attack",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "alliance",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "help_defend",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "team_up",
|
||||
"requiresPlayer": true
|
||||
}
|
||||
],
|
||||
"attack": [
|
||||
{
|
||||
"key": "attack",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "mirv",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "focus",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "finish",
|
||||
"requiresPlayer": true
|
||||
}
|
||||
],
|
||||
"defend": [
|
||||
{
|
||||
"key": "defend",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "dont_attack",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "ally",
|
||||
"requiresPlayer": true
|
||||
}
|
||||
],
|
||||
"greet": [
|
||||
{
|
||||
"key": "hello",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "good_luck",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "have_fun",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "gg",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "nice_to_meet",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "well_played",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "hi_again",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "bye",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "thanks",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "oops",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "trust_me",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "trust_broken",
|
||||
"requiresPlayer": false
|
||||
}
|
||||
],
|
||||
"misc": [
|
||||
{
|
||||
"key": "go",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "strategy",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "fun",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "pr",
|
||||
"requiresPlayer": false
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
{
|
||||
"key": "strong",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "weak",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "mirv_soon",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "number1_warning",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "stalemate",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{
|
||||
"key": "has_allies",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "no_allies",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "betrayed",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "getting_big",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "danger_base",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "saving_for_mirv",
|
||||
"requiresPlayer": true
|
||||
},
|
||||
{
|
||||
"key": "mirv_ready",
|
||||
"requiresPlayer": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
|
||||
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADICAYAAACZBDirAAAAAXNSR0IArs4c6QAADSRJREFUeF7t3E+MXWUZx/Hv/TPTOy1tpdM2FIrSYlRMCcbEoCSEnYsSjDsJGHXhAnVjwoaFBsNGNiSuDFEXEkVwZSIpCzeGhUEWGDUY1ISKoWgtbYGWdqYz949950+saDmn5Tyd5/R+T9K06bz3uc/5PKe/vOee6XT4z9EDrgPuAu4BbgduBPoXrPGPCiigQJsEhsBrwAvAM8BzwFFgVE6is3YmJeT2n//CA2vhtxuYWwu/9TVtOml7VUABBYrABCghuAAcWwvBx4HD5e9LuJWd383AQ8DdwE6gq50CCihwlQmMgePAIeBR4JUSgDcADwL3A7su2BVeZefu6SiggAIrO8I3gCeBx0oA3nd+x/cIsM+dn5eHAgpMgUDZCZZb4IdLAD4FHAS2TcGJe4oKKKBAETgFPFsCsCThXmBGFwUUUGBKBJaBIyUAyx/KgxCf9k7J5D1NBRRY+SxwVEKv/MFDAQUUmDqBywrA8qJNnQ6b6TDb6TBDh577x6m7eDxhBTZKYDQpt64TliYTzjLh3GRyWTu5Sw7Acq883+1xW3+WT88M+FC3x55en62drt88uFFXg++rwBQJlEe4pydj/jka8vfxiN8uL/KH4RInxqPV/95xCUftAFzf9X2o2+fzgy0cnN3M7m6XWTr0O52VDxE9FFBAgSshUIJuOJmwxIRj4zGHls7yi8UzvDYeXtJusHYAllveT/Y38bW5rXyiP8sHuj13fFdi0r6HAgq8p0DZEb45HvHicIkfLpzmd8NzKyFY56gVgGXRrm6Pr85t5d7BNVzb6frIuI6uaxRQ4IoIlLgrIfj0uTP8aOE0b4xHtT4TrBWAg06HO2cGfGvLtezr9d35XZGR+iYKKHApAiv/vWO0zHfOvMXzy4u1doG1AnBHp8s3Nm/nvsGWlYcdHgoooEBGgVOTMT9bPMP3z77NyUmJxPc+agXgnm6P716zgztnB2zy5rfK1K8roMAGCSxOJvx6aYFvn3mTo+PqZ8K1AvCDvT4/2LqTj/dnvf3doMH6tgooUC1QIu+Py0t8/fTxlSfCVUetALyp1+cn23avfP7noYACCmQWODwa8qVTx3h11GAA/nTbbkoQeiiggAKZBUrwfdEAzDwie1NAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSgBAzBK1roKKJBewABMPyIbVECBKAEDMErWugookF7AAEw/IhtUQIEoAQMwSta6CiiQXsAATD8iG1RAgSiBxgNwb7fH97bOs7fbj+rZugoooEAjAkfGQ755+gRHxqPKeh1gUrVq0OnwkV6fAd2qpX5dAQUU2FCBRcb8dTRkcVIZbdQKwLKoRF/53UMBBRTILFBib1xnZ7eWadUxmfls7U0BBRS4TIFaO8DLrO3LFFBAgdQCBmDq8dicAgpEChiAkbrWVkCB1AIGYOrx2JwCCkQKGICRutZWQIHUAgZg6vHYnAIKRAoYgJG61lZAgdQCBmDq8dicAgpEChiAkbrWVkCB1AIGYOrx2JwCCkQKGICRutZWQIHUAgZg6vHYnAIKRAoYgJG61lZAgdQCBmDq8dicAgpEChiAkbrWVkCB1AIGYOrx2JwCCkQKGICRutZWQIHUAgZg6vHYnAIKRAoYgJG61lZAgdQCBmDq8dicAgpEChiAkbrWVkCB1AIGYOrx2JwCCkQKGICRutZWQIHUAgZg6vHYnAIKRAoYgJG61lZAgdQCBmDq8dicAgpEChiAkbrWVkCB1AIGYOrx2JwCCkQKGICRutZWQIHUAgZg6vHYnAIKRAoYgJG61lZAgdQCBmDq8dic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" x="7.500" y="32.813" width="135.000" height="84.375" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
|
||||
<image href="data:image/png;base64,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" x="7.500" y="32.813" width="135.000" height="84.375" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
|
||||
<image href="data:image/png;base64,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" x="7.500" y="32.813" width="135.000" height="84.375" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 28 KiB |