naldo commited on
Commit
4772e29
·
1 Parent(s): fd476cd

Set 100 player limit and Bedrock owner operator

Browse files
minecraft_stack.py CHANGED
@@ -27,7 +27,9 @@ VIA_DIR = BASE / "viaproxy"
27
  # in-place, so using this directory preserves both initial login and token rotation.
28
  RELAY_DIR = PERSIST_ROOT / "relay"
29
  WORLD_DIR = PERSIST_ROOT / "world-20260927-surface-v2"
30
- BOT_USERNAME = os.environ.get("MC_USERNAME", "Naldo_Bot")
 
 
31
  TOOLS_DIR = BASE / "tools"
32
  BIN_DIR = BASE / "bin"
33
  LOG_PATH = BASE / "stack.log"
@@ -194,6 +196,28 @@ def _relay_diagnostics(lines: list[str]) -> dict[str, Any]:
194
  "backend_failures": sum("failed to dial backend" in line or "failed to log in to backend" in line
195
  for line in events),
196
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
 
199
  class MinecraftStack:
@@ -391,8 +415,9 @@ class MinecraftStack:
391
  "allow-flight": "true",
392
  "gamemode": "creative",
393
  "force-gamemode": "true",
394
- "difficulty": "peaceful",
395
- "max-players": "10",
 
396
  "view-distance": "8",
397
  "simulation-distance": "6",
398
  "level-name": "world",
@@ -400,11 +425,12 @@ class MinecraftStack:
400
  "generate-structures": "true",
401
  "motd": "Voyager AI + Bedrock Friends",
402
  }
403
- (JAVA_DIR / "server.properties").write_text(
404
- "\n".join(f"{k}={v}" for k, v in props.items()) + "\n",
405
- encoding="utf-8",
406
- )
407
- self._ensure_persistent_world()
 
408
  return launcher
409
 
410
  def _start_fabric(self) -> None:
@@ -548,7 +574,7 @@ class MinecraftStack:
548
  "nethernet_backend_address": "http://127.0.0.1:19134",
549
  "host_name": HOST_NAME,
550
  "world_name": WORLD_NAME,
551
- "max_players": 10,
552
  "fake_player_count": 0,
553
  "protocol": BEDROCK_PROTOCOL,
554
  "version": BEDROCK_VERSION,
@@ -638,6 +664,26 @@ class MinecraftStack:
638
  all_lines = []
639
  server_lines = []
640
  relay_health = _relay_diagnostics(all_lines[boot:] if all_lines else [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  offline_uuid = uuid.UUID(bytes=hashlib.md5(
642
  f"OfflinePlayer:{BOT_USERNAME}".encode("utf-8")
643
  ).digest(), version=3)
@@ -682,6 +728,9 @@ class MinecraftStack:
682
  "world_persistence": str(WORLD_DIR),
683
  "player_save": player_save,
684
  "relay_health": relay_health,
 
 
 
685
  "bot_username": BOT_USERNAME,
686
  "bot_display_name": "Naldo Bot",
687
  "bedrock_protocol": BEDROCK_PROTOCOL,
 
27
  # in-place, so using this directory preserves both initial login and token rotation.
28
  RELAY_DIR = PERSIST_ROOT / "relay"
29
  WORLD_DIR = PERSIST_ROOT / "world-20260927-surface-v2"
30
+ BOT_USERNAME = os.environ.get("MC_USERNAME", "Naldo_Bot")
31
+ ADMIN_USERNAME = "IffyBinkie6134"
32
+ MAX_PLAYERS = 100
33
  TOOLS_DIR = BASE / "tools"
34
  BIN_DIR = BASE / "bin"
35
  LOG_PATH = BASE / "stack.log"
 
196
  "backend_failures": sum("failed to dial backend" in line or "failed to log in to backend" in line
197
  for line in events),
198
  }
199
+
200
+
201
+ def _offline_player_uuid(username: str) -> str:
202
+ return str(uuid.UUID(bytes=hashlib.md5(
203
+ f"OfflinePlayer:{username}".encode("utf-8")
204
+ ).digest(), version=3))
205
+
206
+
207
+ def _ensure_admin_operator(path: Path, username: str) -> None:
208
+ """Seed the exact offline-mode Java profile used by the Bedrock relay."""
209
+ if not re.fullmatch(r"[A-Za-z0-9_]{1,16}", username):
210
+ raise ValueError("Invalid Minecraft operator username")
211
+ operators = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
212
+ if not isinstance(operators, list):
213
+ raise ValueError("Minecraft ops.json must be a list")
214
+ identity = _offline_player_uuid(username)
215
+ entry = {"uuid": identity, "name": username, "level": 4, "bypassesPlayerLimit": True}
216
+ preserved = [item for item in operators if not (
217
+ isinstance(item, dict) and (item.get("uuid") == identity or item.get("name") == username)
218
+ )]
219
+ preserved.append(entry)
220
+ path.write_text(json.dumps(preserved, indent=2) + "\n", encoding="utf-8")
221
 
222
 
223
  class MinecraftStack:
 
415
  "allow-flight": "true",
416
  "gamemode": "creative",
417
  "force-gamemode": "true",
418
+ "difficulty": "peaceful",
419
+ "max-players": str(MAX_PLAYERS),
420
+ "op-permission-level": "4",
421
  "view-distance": "8",
422
  "simulation-distance": "6",
423
  "level-name": "world",
 
425
  "generate-structures": "true",
426
  "motd": "Voyager AI + Bedrock Friends",
427
  }
428
+ (JAVA_DIR / "server.properties").write_text(
429
+ "\n".join(f"{k}={v}" for k, v in props.items()) + "\n",
430
+ encoding="utf-8",
431
+ )
432
+ _ensure_admin_operator(JAVA_DIR / "ops.json", ADMIN_USERNAME)
433
+ self._ensure_persistent_world()
434
  return launcher
435
 
436
  def _start_fabric(self) -> None:
 
574
  "nethernet_backend_address": "http://127.0.0.1:19134",
575
  "host_name": HOST_NAME,
576
  "world_name": WORLD_NAME,
577
+ "max_players": MAX_PLAYERS,
578
  "fake_player_count": 0,
579
  "protocol": BEDROCK_PROTOCOL,
580
  "version": BEDROCK_VERSION,
 
664
  all_lines = []
665
  server_lines = []
666
  relay_health = _relay_diagnostics(all_lines[boot:] if all_lines else [])
667
+ try:
668
+ operators = json.loads((JAVA_DIR / "ops.json").read_text(encoding="utf-8"))
669
+ expected_uuid = _offline_player_uuid(ADMIN_USERNAME)
670
+ admin_operator = any(
671
+ isinstance(entry, dict) and entry.get("uuid") == expected_uuid
672
+ and entry.get("name") == ADMIN_USERNAME and entry.get("level") == 4
673
+ for entry in operators
674
+ )
675
+ except (OSError, ValueError, TypeError):
676
+ admin_operator = False
677
+ admin_player_file_exists = (WORLD_DIR / "playerdata" / f"{_offline_player_uuid(ADMIN_USERNAME)}.dat").exists()
678
+ try:
679
+ properties = (JAVA_DIR / "server.properties").read_text(encoding="utf-8")
680
+ java_player_limit = int(re.search(r"(?m)^max-players=(\d+)$", properties).group(1))
681
+ except (OSError, AttributeError, ValueError):
682
+ java_player_limit = None
683
+ try:
684
+ xbox_player_limit = int(json.loads((RELAY_DIR / "config.json").read_text(encoding="utf-8"))["max_players"])
685
+ except (OSError, ValueError, KeyError, TypeError):
686
+ xbox_player_limit = None
687
  offline_uuid = uuid.UUID(bytes=hashlib.md5(
688
  f"OfflinePlayer:{BOT_USERNAME}".encode("utf-8")
689
  ).digest(), version=3)
 
728
  "world_persistence": str(WORLD_DIR),
729
  "player_save": player_save,
730
  "relay_health": relay_health,
731
+ "player_limit": {"java": java_player_limit, "xbox_friends": xbox_player_limit},
732
+ "admin_operator": admin_operator,
733
+ "admin_player_file_exists": admin_player_file_exists,
734
  "bot_username": BOT_USERNAME,
735
  "bot_display_name": "Naldo Bot",
736
  "bedrock_protocol": BEDROCK_PROTOCOL,
tests/test_server_admin_config.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import minecraft_stack as mc
6
+
7
+
8
+ def test_operator_seed_preserves_other_operators():
9
+ with tempfile.TemporaryDirectory() as tmp:
10
+ path = Path(tmp) / "ops.json"
11
+ other = {"uuid": "other", "name": "ExistingAdmin", "level": 3, "bypassesPlayerLimit": False}
12
+ path.write_text(json.dumps([other]), encoding="utf-8")
13
+ mc._ensure_admin_operator(path, "IffyBinkie6134")
14
+ mc._ensure_admin_operator(path, "IffyBinkie6134")
15
+ entries = json.loads(path.read_text(encoding="utf-8"))
16
+ assert entries[0] == other
17
+ assert len(entries) == 2
18
+ assert entries[1] == {
19
+ "uuid": mc._offline_player_uuid("IffyBinkie6134"),
20
+ "name": "IffyBinkie6134", "level": 4, "bypassesPlayerLimit": True,
21
+ }
22
+ try:
23
+ mc._ensure_admin_operator(path, "bad name; op someone")
24
+ except ValueError:
25
+ pass
26
+ else:
27
+ raise AssertionError("invalid username accepted")
28
+ assert json.loads(path.read_text(encoding="utf-8")) == entries
29
+
30
+
31
+ def test_status_reports_real_configured_limits_and_operator():
32
+ with tempfile.TemporaryDirectory() as tmp:
33
+ base = Path(tmp)
34
+ old = (mc.JAVA_DIR, mc.RELAY_DIR, mc.WORLD_DIR, mc.LOG_PATH)
35
+ try:
36
+ mc.JAVA_DIR, mc.RELAY_DIR = base / "java", base / "relay"
37
+ mc.WORLD_DIR, mc.LOG_PATH = base / "world", base / "stack.log"
38
+ mc.JAVA_DIR.mkdir()
39
+ mc.RELAY_DIR.mkdir()
40
+ mc.WORLD_DIR.mkdir()
41
+ (mc.JAVA_DIR / "server.properties").write_text("max-players=100\n", encoding="utf-8")
42
+ (mc.RELAY_DIR / "config.json").write_text('{"max_players":100}', encoding="utf-8")
43
+ mc._ensure_admin_operator(mc.JAVA_DIR / "ops.json", mc.ADMIN_USERNAME)
44
+ status = mc.MinecraftStack().status()
45
+ assert status["player_limit"] == {"java": 100, "xbox_friends": 100}
46
+ assert status["admin_operator"] is True
47
+ assert status["admin_player_file_exists"] is False
48
+ (mc.WORLD_DIR / "playerdata").mkdir()
49
+ (mc.WORLD_DIR / "playerdata" / f"{mc._offline_player_uuid(mc.ADMIN_USERNAME)}.dat").write_bytes(b"saved")
50
+ assert mc.MinecraftStack().status()["admin_player_file_exists"] is True
51
+ (mc.JAVA_DIR / "ops.json").write_text("[]", encoding="utf-8")
52
+ assert mc.MinecraftStack().status()["admin_operator"] is False
53
+ finally:
54
+ mc.JAVA_DIR, mc.RELAY_DIR, mc.WORLD_DIR, mc.LOG_PATH = old
55
+
56
+
57
+ if __name__ == "__main__":
58
+ test_operator_seed_preserves_other_operators()
59
+ test_status_reports_real_configured_limits_and_operator()
60
+ print("PASS player limit and persistent operator seed")