Skip to content

Commit 7c85bf4

Browse files
committed
Add APK server emulator (Python TCP server for DragonSoul protocol)
Full server-side implementation reverse-engineered from the APK: - protocol.py: XOR cipher, binary frame reader/writer - messages.py: BootData1 builder (all 97 UserExtra fields, correct field order) - handlers.py: ClientInfo1 → BootData1 dispatch, all message handlers - gamedata.py: hero stats, campaign levels, XP tables - combat.py: battle simulation - config.py: server configuration - server.py: async TCP + aiohttp gateway - content.1.tab.txt: StatData for campaign chapter availability This server handles: player name, gold, diamonds, stamina, team level, heroes, campaign progress, arena, guilds, etc. Note: accidentally committed to main — cherry-picked to correct branch. https://claude.ai/code/session_01XQ6Lv2cEvrtZLgGCFgDDjT
1 parent 5531038 commit 7c85bf4

8 files changed

Lines changed: 4891 additions & 0 deletions

File tree

Apk server/combat (3).py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
DragonSoul Server Emulator - Turn-based Combat Simulation
3+
Simple combat engine for campaign battles and PvP.
4+
"""
5+
6+
import random
7+
import logging
8+
9+
logger = logging.getLogger("combat")
10+
11+
12+
def _make_combatant(unit, team_id):
13+
"""Create a combatant dict from a hero or NPC dict."""
14+
return {
15+
"name": unit.get("name", "Unknown"),
16+
"team": team_id,
17+
"role": unit.get("role", "dps"),
18+
"hp": unit.get("health", 1000),
19+
"max_hp": unit.get("health", 1000),
20+
"atk": unit.get("attack", 100),
21+
"armor": unit.get("armor", 30),
22+
"magic_resist": unit.get("magic_resist", 20),
23+
"intellect": unit.get("intellect", 50),
24+
"speed": unit.get("speed", 50),
25+
"energy": 0,
26+
"alive": True,
27+
}
28+
29+
30+
def simulate_battle(attacker_heroes, defender_heroes):
31+
"""
32+
Run a turn-based combat simulation.
33+
34+
Args:
35+
attacker_heroes: list of hero stat dicts (from get_hero_stats or NPC dicts)
36+
defender_heroes: list of defender stat dicts
37+
38+
Returns:
39+
(won: bool, battle_log: list of log entry strings)
40+
"""
41+
# Build combatants
42+
attackers = [_make_combatant(h, 0) for h in attacker_heroes]
43+
defenders = [_make_combatant(h, 1) for h in defender_heroes]
44+
all_units = attackers + defenders
45+
46+
log = []
47+
turn = 0
48+
max_turns = 100
49+
50+
while turn < max_turns:
51+
turn += 1
52+
53+
# Sort by speed (descending), alive only
54+
alive_units = [u for u in all_units if u["alive"]]
55+
alive_units.sort(key=lambda u: u["speed"], reverse=True)
56+
57+
if not alive_units:
58+
break
59+
60+
for unit in alive_units:
61+
if not unit["alive"]:
62+
continue
63+
64+
# Determine enemy team
65+
if unit["team"] == 0:
66+
enemies = [u for u in defenders if u["alive"]]
67+
else:
68+
enemies = [u for u in attackers if u["alive"]]
69+
70+
allies = [u for u in all_units if u["alive"] and u["team"] == unit["team"] and u is not unit]
71+
72+
if not enemies:
73+
break
74+
75+
# Healers heal lowest HP ally (or self) instead of attacking
76+
if unit["role"] == "healer":
77+
# Heal lowest HP ally
78+
heal_targets = [u for u in all_units if u["alive"] and u["team"] == unit["team"]]
79+
if heal_targets:
80+
target = min(heal_targets, key=lambda u: u["hp"] / max(u["max_hp"], 1))
81+
heal_amount = max(1, unit["intellect"] // 2)
82+
target["hp"] = min(target["max_hp"], target["hp"] + heal_amount)
83+
log.append(
84+
f"T{turn}: {unit['name']} heals {target['name']} for {heal_amount} HP"
85+
)
86+
unit["energy"] = min(100, unit["energy"] + 20)
87+
continue
88+
89+
# Pick target: enemy with lowest HP
90+
target = min(enemies, key=lambda u: u["hp"])
91+
92+
# Check for ultimate (energy >= 100)
93+
is_ultimate = unit["energy"] >= 100
94+
damage_mult = 3 if is_ultimate else 1
95+
96+
# Calculate damage
97+
raw_damage = unit["atk"] * damage_mult
98+
damage = max(1, raw_damage - target["armor"])
99+
100+
# Critical hit: 15% chance, 2x damage
101+
is_crit = random.random() < 0.15
102+
if is_crit:
103+
damage *= 2
104+
105+
target["hp"] -= damage
106+
107+
# Log the action
108+
action = "ULTIMATE" if is_ultimate else "attacks"
109+
crit_text = " (CRIT!)" if is_crit else ""
110+
log.append(
111+
f"T{turn}: {unit['name']} {action} {target['name']} for {damage} damage{crit_text}"
112+
)
113+
114+
# Energy management
115+
if is_ultimate:
116+
unit["energy"] = 0
117+
else:
118+
unit["energy"] = min(100, unit["energy"] + 20)
119+
120+
# Target takes energy gain from being hit
121+
target["energy"] = min(100, target["energy"] + 10)
122+
123+
# Check if target died
124+
if target["hp"] <= 0:
125+
target["alive"] = False
126+
log.append(f"T{turn}: {target['name']} is defeated!")
127+
128+
# Check win conditions
129+
attacker_alive = any(u["alive"] for u in attackers)
130+
defender_alive = any(u["alive"] for u in defenders)
131+
132+
if not defender_alive:
133+
log.append(f"Battle won in {turn} turns!")
134+
return True, log
135+
if not attacker_alive:
136+
log.append(f"Battle lost in {turn} turns.")
137+
return False, log
138+
139+
# Timeout - defender wins
140+
log.append(f"Battle timed out after {max_turns} turns. Defender wins.")
141+
return False, log
142+
143+
144+
def calculate_drops(chapter, level, won):
145+
"""
146+
Calculate rewards for a campaign battle.
147+
Returns a dict with gold, xp, hero_xp, and possible item drops.
148+
149+
Reward scaling (based on original DragonSoul values):
150+
- Chapter 0: 150-530 gold, 60-240 XP per level
151+
- Chapter 1+: scales up significantly
152+
"""
153+
if not won:
154+
return {"gold": 0, "xp": 0, "hero_xp": 0, "items": []}
155+
156+
# Gold: base 150, scales with chapter and level
157+
base_gold = 150 + 200 * chapter + 20 * level
158+
# Add some randomness (±20%)
159+
gold = int(base_gold * (0.8 + random.random() * 0.4))
160+
161+
# Team XP: base 60, scales with chapter and level
162+
base_xp = 60 + 100 * chapter + 10 * level
163+
xp = int(base_xp * (0.9 + random.random() * 0.2))
164+
165+
# Hero XP: heroes in the lineup get XP from combat
166+
# Original game gives ~100-500 hero XP per campaign level
167+
hero_xp = 100 + 50 * chapter + 25 * level
168+
169+
# Random item drops (50% chance for 1 item, 15% chance for 2)
170+
items = []
171+
if random.random() < 0.50:
172+
item_id = random.randint(1, 20)
173+
items.append(item_id)
174+
if random.random() < 0.15:
175+
item_id = random.randint(1, 20)
176+
items.append(item_id)
177+
178+
return {
179+
"gold": gold,
180+
"xp": xp,
181+
"hero_xp": hero_xp,
182+
"items": items,
183+
}

Apk server/config (4).py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
DragonSoul Server Emulator - Configuration
3+
"""
4+
5+
# Server network settings
6+
HOST = "0.0.0.0"
7+
PORT = 9500
8+
9+
# Database
10+
import os as _os
11+
_BASE_DIR = _os.path.dirname(_os.path.abspath(__file__))
12+
DB_PATH = _os.path.join(_BASE_DIR, "dragonsoul.db")
13+
14+
# Game version info
15+
GAME_VERSION = 155
16+
FULL_VERSION = 15500
17+
SDK_VERSION = 1
18+
19+
# Server shard info
20+
SHARD_ID = 1
21+
SERVER_NAME = "DragonSoul Emulator"
22+
MAX_TEAM_LEVEL = 130
23+
NUM_CHAPTERS = 15
24+
25+
# Starting resources for new players
26+
STARTING_GOLD = 0 # Real game starts with 0
27+
STARTING_STAMINA = 60 # Real game starts with 60
28+
STARTING_DIAMONDS = 0 # Real game starts with 0
29+
STARTING_XP = 0
30+
STARTING_TEAM_LEVEL = 1
31+
MAX_STAMINA = 60 # Increases with team level
32+
33+
# XOR key (unsigned bytes)
34+
XOR_KEY = [0x02, 0xA6, 0xEE, 0xD5, 0xD0, 0xBC, 0x6A, 0x98]
35+
36+
# Address the HTTP login server tells clients to connect to for the TCP game server
37+
# Set to "auto" to auto-detect your LAN IP at startup
38+
# Or manually set: "192.168.1.42:9500", "myserver.ddns.net:9500", etc.
39+
GAME_SERVER_ADVERTISE = "auto"
40+
HTTP_PORT = 8080
41+
42+
# ── Player Identity Mode ─────────────────────────────────────────────
43+
# SINGLE_PLAYER_MODE = True (default)
44+
# → All connections with empty device_id reuse the same player.
45+
# Ideal for local testing with one player. Progress always persists.
46+
#
47+
# SINGLE_PLAYER_MODE = False
48+
# → Each connection with empty device_id gets a unique identity.
49+
# Required for multiplayer. Clients must send back their assigned
50+
# userID on reconnect to recover their account.
51+
SINGLE_PLAYER_MODE = True
52+
53+
# Logging
54+
LOG_LEVEL = "INFO"
55+
LOG_MESSAGES = True # Log all incoming/outgoing message types
56+
57+
# Compression chunk size
58+
ZLIB_CHUNK_SIZE = 512

0 commit comments

Comments
 (0)