Skip to content

Commit e300654

Browse files
authored
Merge branch 'v2' into bookworm
2 parents d8a4ace + 1aa422b commit e300654

File tree

7 files changed

+251
-109
lines changed

7 files changed

+251
-109
lines changed

Dockerfile

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ RUN apt-get update \
88
&& \
99
apt-get install -y --no-install-recommends --no-install-suggests \
1010
python3 \
11+
python3-pip \
1112
lib32stdc++6 \
1213
lib32gcc-s1 \
1314
libcurl4 \
@@ -17,20 +18,21 @@ RUN apt-get update \
1718
libstdc++6 \
1819
libssl3 \
1920
libc6 \
21+
git \
2022
&& \
2123
apt-get remove --purge -y \
2224
&& \
2325
apt-get clean autoclean \
2426
&& \
2527
apt-get autoremove -y \
2628
&& \
27-
rm -rf /var/lib/apt/lists/* \
28-
&& \
29-
mkdir -p /steamcmd \
30-
&& \
31-
wget -qO- 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz' | tar zxf - -C /steamcmd
29+
rm -rf /var/lib/apt/lists/*
30+
31+
RUN pip3 install -U zstandard "git+https://github.com/brettmayson/valvepythonsteam#egg=steam[client]"
32+
33+
ENV PYTHONUNBUFFERED=1
3234

33-
ENV ARMA_BINARY=./arma3server
35+
ENV ARMA_BINARY=./arma3server_x64
3436
ENV ARMA_CONFIG=main.cfg
3537
ENV ARMA_PARAMS=
3638
ENV ARMA_PROFILE=main
@@ -40,10 +42,8 @@ ENV ARMA_CDLC=
4042
ENV HEADLESS_CLIENTS=0
4143
ENV HEADLESS_CLIENTS_PROFILE="\$profile-hc-\$i"
4244
ENV PORT=2302
43-
ENV STEAM_BRANCH=public
44-
ENV STEAM_BRANCH_PASSWORD=
45-
ENV STEAM_ADDITIONAL_DEPOT=
4645
ENV MODS_LOCAL=true
46+
ENV CLEAR_KEYS=true
4747
ENV MODS_PRESET=
4848
ENV SKIP_INSTALL=false
4949

@@ -55,14 +55,7 @@ EXPOSE 2306/udp
5555

5656
WORKDIR /arma3
5757

58-
VOLUME /steamcmd
59-
VOLUME /arma3/addons
60-
VOLUME /arma3/enoch
61-
VOLUME /arma3/expansion
62-
VOLUME /arma3/jets
63-
VOLUME /arma3/heli
64-
VOLUME /arma3/orange
65-
VOLUME /arma3/argo
58+
VOLUME /arma3/server
6659

6760
STOPSIGNAL SIGINT
6861

README.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@ An Arma 3 Dedicated Server. Updates to the latest version every time it is resta
1414
-p 2304:2304/udp \
1515
-p 2305:2305/udp \
1616
-p 2306:2306/udp \
17-
-v path/to/missions:/arma3/mpmissions \
18-
-v path/to/configs:/arma3/configs \
19-
-v path/to/mods:/arma3/mods \
20-
-v path/to/servermods:/arma3/servermods \
21-
-e ARMA_CONFIG=main.cfg \
17+
-v path/to/missions:/arma3/server/mpmissions \
18+
-v path/to/configs:/arma3/server/configs \
19+
-v path/to/mods:/arma3/server/mods \
20+
-v path/to/servermods:/arma3/server/servermods \
2221
-e STEAM_USER=myusername \
2322
-e STEAM_PASSWORD=mypassword \
24-
ghcr.io/brettmayson/arma3server/arma3server:latest
23+
ghcr.io/brettmayson/arma3server/arma3server:v2
2524
```
2625

2726
### docker-compose
@@ -42,35 +41,34 @@ Use `docker-compose up -d` to start the server, detached.
4241

4342
See [Docker-compose](https://docs.docker.com/compose/install/#install-compose) for an installation guide.
4443

45-
Profiles are saved in `/arma3/configs/profiles`
44+
Profiles are saved in `/arma3/server/configs/profiles`
4645

4746
## Parameters
4847

4948
| Parameter | Function | Default |
5049
| ------------- |-------------- | - |
5150
| `-p 2302-2306` | Ports required by Arma 3 |
52-
| `-v /arma3/mpmission` | Folder with MP Missions |
53-
| `-v /arma3/configs` | Folder containing config files |
54-
| `-v /arma3/mods` | Mods that will be loaded by clients |
55-
| `-v /arma3/servermods` | Mods that will only be loaded by the server |
51+
| `-v /arma3/server/mpmission` | Folder with MP Missions |
52+
| `-v /arma3/server/configs` | Folder containing config files |
53+
| `-v /arma3/server/mods` | Mods that will be loaded by clients |
54+
| `-v /arma3/server/servermods` | Mods that will only be loaded by the server |
55+
| `-v /arma3/server` | Folder containing the server files |
5656
| `-e PORT` | Port used by the server, (uses PORT to PORT+3) | 2302 |
57-
| `-e ARMA_BINARY` | Arma 3 server binary to use, `./arma3server_x64` for x64 | `./arma3server` |
58-
| `-e ARMA_CONFIG` | Config file to load from `/arma3/configs` | `main.cfg` |
57+
| `-e ARMA_BINARY` | Arma 3 server binary to use | `./arma3server` |
58+
| `-e ARMA_CONFIG` | Config file to load from `/arma3/server/configs` | `main.cfg` |
5959
| `-e ARMA_PARAMS` | Additional Arma CLI parameters |
60-
| `-e ARMA_PROFILE` | Profile name, stored in `/arma3/configs/profiles` | `main` |
60+
| `-e ARMA_PROFILE` | Profile name, stored in `/arma3/server/configs/profiles` | `main` |
6161
| `-e ARMA_WORLD` | World to load on startup | `empty` |
6262
| `-e ARMA_LIMITFPS` | Maximum FPS | `1000` |
63-
| `-e ARMA_CDLC` | cDLCs to load |
64-
| `-e STEAM_BRANCH` | Steam branch used by steamcmd | `public` |
65-
| `-e STEAM_BRANCH_PASSWORD` | Steam branch password used by steamcmd |
63+
| `-e ARMA_CDLC` | cDLCs to load, separated by semicolons | - |
6664
| `-e STEAM_USER` | Steam username used to login to steamcmd |
6765
| `-e STEAM_PASSWORD` | Steam password |
6866
| `-e HEADLESS_CLIENTS` | Launch n number of headless clients | `0` |
6967
| `-e HEADLESS_CLIENTS_PROFILE` | Headless client profile name (supports placeholders) | `$profile-hc-$i` |
7068
| `-e MODS_LOCAL` | Should the mods folder be loaded | `true` |
7169
| `-e MODS_PRESET` | An Arma 3 Launcher preset to load |
7270
| `-e SKIP_INSTALL` | Skip Arma 3 installation | `false` |
73-
| `-e CLEAR_KEYS` | Clear the keys directory every launch (keys will still be copied from mods) | `false` |
71+
| `-e CLEAR_KEYS` | Clear the keys directory every launch (keys will still be copied from mods) | `true` |
7472

7573
The Steam account does not need to own Arma 3, but must have Steam Guard disabled.
7674

@@ -82,10 +80,10 @@ To use a Creator DLC the `STEAM_BRANCH` must be set to `creatordlc`
8280

8381
| Name | Flag |
8482
| ---- | ---- |
85-
| [CSLA Iron Curtain](https://store.steampowered.com/app/1294440/Arma_3_Creator_DLC_CSLA_Iron_Curtain/) | CSLA |
86-
| [Global Mobilization - Cold War Germany](https://store.steampowered.com/app/1042220/Arma_3_Creator_DLC_Global_Mobilization__Cold_War_Germany/) | GM |
83+
| [CSLA Iron Curtain](https://store.steampowered.com/app/1294440/Arma_3_Creator_DLC_CSLA_Iron_Curtain/) | csla |
84+
| [Global Mobilization - Cold War Germany](https://store.steampowered.com/app/1042220/Arma_3_Creator_DLC_Global_Mobilization__Cold_War_Germany/) | gm |
8785
| [S.O.G. Prairie Fire](https://store.steampowered.com/app/1227700/Arma_3_Creator_DLC_SOG_Prairie_Fire) | vn |
88-
| [Western Sahara](https://store.steampowered.com/app/1681170/Arma_3_Creator_DLC_Western_Sahara/) | WS |
86+
| [Western Sahara](https://store.steampowered.com/app/1681170/Arma_3_Creator_DLC_Western_Sahara/) | ws |
8987
| [Spearhead 1944](https://store.steampowered.com/app/1175380/Arma_3_Creator_DLC_Spearhead_1944/) | spe |
9088
| [Reaction Forces](https://store.steampowered.com/app/2647760/Arma_3_Creator_DLC_Reaction_Forces/) | rf |
9189
| [Expeditionary Forces](https://store.steampowered.com/app/2647830/Arma_3_Creator_DLC_Expeditionary_Forces/) | ef |
@@ -110,7 +108,7 @@ Bohemia-updated list of codes here: <https://community.bistudio.com/wiki/Categor
110108

111109
### Workshop
112110

113-
Set the environment variable `MODS_PRESET` to the HTML preset file exported from the Arma 3 Launcher. The path can be local file or a URL. A volume can be created at `/arma3/steamapps/workshop/content/107410` to preserve the mods between containers.
111+
Set the environment variable `MODS_PRESET` to the HTML preset file exported from the Arma 3 Launcher. The path can be local file or a URL. A volume can be created at `/arma3/server/workshop/` to preserve the mods between containers separately from the main `/arma3/server` volume.
114112

115113
`-e MODS_PRESET="my_mods.html"`
116114

api.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from steam.client import SteamClient
2+
from steam.client.cdn import CDNClient, CDNDepotFile
3+
import os
4+
import hashlib
5+
import json
6+
import time
7+
import sys
8+
9+
ARMA3_SERVER_APP_ID = 233780
10+
CACHE_DIR = "cache"
11+
MANIFEST_CACHE_FILE = os.path.join(CACHE_DIR, "manifests.json")
12+
CACHE_EXPIRY_SECONDS = 5 * 60
13+
14+
CDLC_IDS = {
15+
"csla": 233793,
16+
"gm": 233792,
17+
"vn": 233794,
18+
"ws": 233795,
19+
"spe": 233788,
20+
"rf": 233799,
21+
"ef": 233798
22+
}
23+
24+
def login(username, password):
25+
client = SteamClient()
26+
client.login(username, password)
27+
print("Logged in to Steam as", client.user.name)
28+
return client
29+
30+
def load_cached_manifests():
31+
"""Load cached manifest data if it exists and is not expired"""
32+
if not os.path.exists(MANIFEST_CACHE_FILE):
33+
return None
34+
35+
try:
36+
with open(MANIFEST_CACHE_FILE, 'r') as f:
37+
cache_data = json.load(f)
38+
39+
if time.time() - cache_data.get('timestamp', 0) > CACHE_EXPIRY_SECONDS:
40+
print("Manifest cache expired, will refetch...")
41+
return None
42+
43+
return cache_data.get('manifests', [])
44+
except (json.JSONDecodeError, FileNotFoundError):
45+
print("Cache file corrupted or missing, will refetch...")
46+
return None
47+
48+
def save_manifests_to_cache(manifests):
49+
"""Save manifest data to cache with timestamp"""
50+
os.makedirs(CACHE_DIR, exist_ok=True)
51+
52+
manifest_data = []
53+
for manifest in manifests:
54+
manifest_data.append({
55+
'name': manifest.name,
56+
'gid': manifest.gid,
57+
'depot_id': manifest.depot_id
58+
})
59+
60+
cache_data = {
61+
'timestamp': time.time(),
62+
'manifests': manifest_data
63+
}
64+
65+
with open(MANIFEST_CACHE_FILE, 'w') as f:
66+
json.dump(cache_data, f, indent=2)
67+
68+
print(f"Manifest data cached to {MANIFEST_CACHE_FILE}")
69+
70+
def download_depot(client, depot_id):
71+
cdn_client = CDNClient(client)
72+
73+
cached_manifests = load_cached_manifests()
74+
75+
if cached_manifests:
76+
manifests = cached_manifests
77+
print("Got manifests from cache for ARMA3 server app ID:", ARMA3_SERVER_APP_ID)
78+
else:
79+
print("Fetching fresh manifests from Steam...")
80+
manifests_obj = cdn_client.get_manifests(ARMA3_SERVER_APP_ID, branch="creatordlc")
81+
82+
save_manifests_to_cache(manifests_obj)
83+
84+
manifests = []
85+
for manifest in manifests_obj:
86+
manifests.append({
87+
'name': manifest.name,
88+
'gid': manifest.gid,
89+
'depot_id': manifest.depot_id
90+
})
91+
92+
target_manifest = next((m for m in manifests if m['depot_id'] == depot_id), None)
93+
if not target_manifest:
94+
print(f"No manifest found for depot ID {depot_id}")
95+
return
96+
97+
print(f"Downloading Manifest ID: {target_manifest['gid']}, Depot ID: {target_manifest['depot_id']}")
98+
99+
files_generator = cdn_client.iter_files(ARMA3_SERVER_APP_ID, branch="creatordlc", filter_func=lambda d_id, depot_info: d_id == target_manifest['depot_id'])
100+
files = list(files_generator)
101+
files = [f for f in files if f.is_file]
102+
print(f"Found {len(files)} files to download")
103+
download_files(client, cdn_client, files, destination="server/")
104+
105+
def download_workshop(client, workshop_id):
106+
cdn_client = CDNClient(client)
107+
workshop_manifest = cdn_client.get_manifest_for_workshop_item(workshop_id)
108+
files_generator = workshop_manifest.iter_files()
109+
files = list(files_generator)
110+
files = [f for f in files if f.is_file]
111+
print(f"Found {len(files)} files to download")
112+
download_files(client, cdn_client, files, destination=f"server/workshop/{workshop_id}/")
113+
114+
def download_files(client, cdn_client, files, destination):
115+
print(f"Verifying {len(files)} files...")
116+
files_to_download = []
117+
118+
for i, file in enumerate(files):
119+
file.local = os.path.join(destination, file.filename).lower()
120+
121+
if os.path.exists(file.local):
122+
with open(file.local, 'rb') as f:
123+
existing_hash = hashlib.sha1(f.read()).hexdigest()
124+
expected_hash = file.sha_content.hex() if isinstance(file.sha_content, bytes) else file.sha_content
125+
if existing_hash == expected_hash:
126+
if file.is_executable:
127+
os.chmod(file.local, 0o755)
128+
continue
129+
files_to_download.append(file)
130+
131+
print(f"Need to download {len(files_to_download)} files...")
132+
133+
for i, file in enumerate(files_to_download):
134+
print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename} ({file.size} bytes)")
135+
_download_single_file(file)
136+
137+
print("All files downloaded successfully.")
138+
139+
def _download_single_file(file):
140+
if file.local and os.path.dirname(file.local) != "":
141+
os.makedirs(os.path.dirname(file.local), exist_ok=True)
142+
143+
chunk_size = 1024 * 1024 # 1MB chunks
144+
downloaded = 0
145+
failed = False
146+
147+
with open(file.local, 'wb') as f:
148+
while downloaded < file.size:
149+
remaining = file.size - downloaded
150+
read_size = min(chunk_size, remaining)
151+
152+
chunk = file.read(read_size)
153+
if not chunk:
154+
break
155+
156+
f.write(chunk)
157+
downloaded += len(chunk)
158+
159+
if downloaded % (10 * 1024 * 1024) == 0:
160+
percent = (downloaded / file.size) * 100
161+
print(f" Progress: {percent:.1f}% ({downloaded}/{file.size} bytes)")
162+
163+
if failed:
164+
print(f"Failed to download {file.filename}")
165+
else:
166+
if file.is_executable:
167+
os.chmod(file.local, 0o755)
168+
print(f"✓ Downloaded {file.filename}")
169+
170+
if __name__ == "__main__":
171+
import os
172+
import sys
173+
174+
if len(sys.argv) != 3:
175+
print("Usage: python api.py <username> <password>")
176+
sys.exit(1)
177+
178+
username = sys.argv[1]
179+
password = sys.argv[2]
180+
181+
client = login(username, password)
182+
if client:
183+
download_depot(client, 233785) # Western Sahara
184+
download_workshop(client, 463939057) # Example workshop ID for ACE3 mod

docker-compose.yml

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,15 @@ version: '3.8'
22
services:
33
arma3:
44
build: .
5-
image: ghcr.io/brettmayson/arma3server/arma3server:latest
5+
image: ghcr.io/brettmayson/arma3server/arma3server:v2
66
platform: linux/amd64
77
container_name: arma3
88
network_mode: host
99
volumes:
10-
- './missions:/arma3/mpmissions'
11-
- './configs:/arma3/configs'
12-
- './mods:/arma3/mods'
13-
- './servermods:/arma3/servermods'
14-
- 'addons:/arma3/addons'
15-
- 'argo:/arma3/argo'
16-
- 'enoch:/arma3/enoch'
17-
- 'expansion:/arma3/expansion'
18-
- 'heli:/arma3/heli'
19-
- 'jets:/arma3/jets'
20-
- 'orange:/arma3/orange'
21-
- 'steamcmd:/steamcmd'
10+
- './configs:/arma3/server/configs'
11+
- './mods:/arma3/server/mods'
12+
- './servermods:/arma3/server/servermods'
13+
- './server:/arma3/server/'
2214
env_file: .env
2315
restart: unless-stopped
2416
volumes:

keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def copy(moddir):
88
if len(keys) > 0:
99
for key in keys:
1010
if not os.path.isdir(key):
11-
shutil.copy2(key, "/arma3/keys")
11+
shutil.copy2(key, "/arma3/server/keys")
1212
else:
1313
print("Missing keys:", moddir)
1414

0 commit comments

Comments
 (0)