mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Fix DiscordRpc to send presence only after game join, apply C# best practices, remove temp E2E scripts
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/80d151a5-aee7-4ae6-8497-9e7487d25136
This commit is contained in:
parent
6c1449439c
commit
221b82643d
5 changed files with 29 additions and 817 deletions
|
|
@ -1,222 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fake Discord IPC server.
|
||||
Listens on a Unix domain socket at the Discord IPC path and handles
|
||||
the discord-rpc-csharp handshake + SET_ACTIVITY commands.
|
||||
Logs all received presence updates to stdout and a log file.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
# Discord IPC opcodes
|
||||
OP_HANDSHAKE = 0
|
||||
OP_FRAME = 1
|
||||
OP_CLOSE = 2
|
||||
OP_PING = 3
|
||||
OP_PONG = 4
|
||||
|
||||
LOG_FILE = "/tmp/e2e-test/discord_rpc.log"
|
||||
SOCKET_PATH = None
|
||||
|
||||
presence_received = threading.Event()
|
||||
all_presences = []
|
||||
|
||||
|
||||
def log(msg):
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
line = f"[{timestamp}] [FakeDiscordIPC] {msg}"
|
||||
print(line, flush=True)
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def get_socket_path():
|
||||
"""Determine where discord-rpc-csharp will look for the IPC socket."""
|
||||
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
|
||||
if runtime_dir:
|
||||
return os.path.join(runtime_dir, "discord-ipc-0")
|
||||
|
||||
tmpdir = os.environ.get("TMPDIR", "/tmp")
|
||||
return os.path.join(tmpdir, "discord-ipc-0")
|
||||
|
||||
|
||||
def read_message(conn):
|
||||
"""Read a Discord IPC message: 4-byte LE opcode + 4-byte LE length + JSON."""
|
||||
header = b""
|
||||
while len(header) < 8:
|
||||
chunk = conn.recv(8 - len(header))
|
||||
if not chunk:
|
||||
return None, None
|
||||
header += chunk
|
||||
|
||||
opcode, length = struct.unpack("<II", header)
|
||||
|
||||
data = b""
|
||||
while len(data) < length:
|
||||
chunk = conn.recv(length - len(data))
|
||||
if not chunk:
|
||||
return None, None
|
||||
data += chunk
|
||||
|
||||
try:
|
||||
payload = json.loads(data.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
payload = data
|
||||
|
||||
return opcode, payload
|
||||
|
||||
|
||||
def send_message(conn, opcode, payload):
|
||||
"""Send a Discord IPC message."""
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
header = struct.pack("<II", opcode, len(data))
|
||||
conn.sendall(header + data)
|
||||
|
||||
|
||||
def handle_client(conn, addr):
|
||||
"""Handle a single Discord RPC client connection."""
|
||||
log(f"Client connected: {addr}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
opcode, payload = read_message(conn)
|
||||
if opcode is None:
|
||||
log("Client disconnected")
|
||||
break
|
||||
|
||||
if opcode == OP_HANDSHAKE:
|
||||
client_id = payload.get("client_id", "unknown")
|
||||
log(f"HANDSHAKE received - client_id: {client_id}, version: {payload.get('v')}")
|
||||
|
||||
# Send READY response
|
||||
ready_response = {
|
||||
"cmd": "DISPATCH",
|
||||
"evt": "READY",
|
||||
"data": {
|
||||
"v": 1,
|
||||
"config": {
|
||||
"cdn_host": "cdn.discordapp.com",
|
||||
"api_endpoint": "https://discord.com/api",
|
||||
"environment": "production"
|
||||
},
|
||||
"user": {
|
||||
"id": "123456789012345678",
|
||||
"username": "TestUser",
|
||||
"discriminator": "0",
|
||||
"avatar": None,
|
||||
"global_name": "TestUser"
|
||||
}
|
||||
},
|
||||
"nonce": None
|
||||
}
|
||||
send_message(conn, OP_FRAME, ready_response)
|
||||
log("Sent READY response (user: TestUser)")
|
||||
|
||||
elif opcode == OP_FRAME:
|
||||
cmd = payload.get("cmd", "UNKNOWN")
|
||||
nonce = payload.get("nonce")
|
||||
|
||||
if cmd == "SET_ACTIVITY":
|
||||
activity = payload.get("args", {}).get("activity", {})
|
||||
log(f"*** SET_ACTIVITY received ***")
|
||||
log(f" Details : {activity.get('details', 'N/A')}")
|
||||
log(f" State : {activity.get('state', 'N/A')}")
|
||||
|
||||
assets = activity.get("assets", {})
|
||||
if assets:
|
||||
log(f" Large : {assets.get('large_image', 'N/A')} ({assets.get('large_text', '')})")
|
||||
log(f" Small : {assets.get('small_image', 'N/A')} ({assets.get('small_text', '')})")
|
||||
|
||||
timestamps = activity.get("timestamps", {})
|
||||
if timestamps:
|
||||
log(f" Start : {timestamps.get('start', 'N/A')}")
|
||||
|
||||
party = activity.get("party", {})
|
||||
if party:
|
||||
log(f" Party : {party.get('id', '')} size={party.get('size', [])}")
|
||||
|
||||
all_presences.append(activity)
|
||||
presence_received.set()
|
||||
|
||||
# Send success response
|
||||
response = {
|
||||
"cmd": "SET_ACTIVITY",
|
||||
"evt": None,
|
||||
"data": activity,
|
||||
"nonce": nonce
|
||||
}
|
||||
send_message(conn, OP_FRAME, response)
|
||||
log("Sent SET_ACTIVITY acknowledgement")
|
||||
|
||||
else:
|
||||
log(f"FRAME received - cmd: {cmd}, payload: {json.dumps(payload, indent=2)[:200]}")
|
||||
# Generic ack
|
||||
response = {
|
||||
"cmd": cmd,
|
||||
"evt": None,
|
||||
"data": {},
|
||||
"nonce": nonce
|
||||
}
|
||||
send_message(conn, OP_FRAME, response)
|
||||
|
||||
elif opcode == OP_CLOSE:
|
||||
log(f"CLOSE received: {payload}")
|
||||
break
|
||||
|
||||
elif opcode == OP_PING:
|
||||
log("PING received, sending PONG")
|
||||
send_message(conn, OP_PONG, payload)
|
||||
|
||||
else:
|
||||
log(f"Unknown opcode {opcode}: {payload}")
|
||||
|
||||
except Exception as e:
|
||||
log(f"Error handling client: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
log("Client connection closed")
|
||||
|
||||
|
||||
def main():
|
||||
global SOCKET_PATH
|
||||
|
||||
SOCKET_PATH = get_socket_path()
|
||||
|
||||
# Clean up old socket
|
||||
if os.path.exists(SOCKET_PATH):
|
||||
os.unlink(SOCKET_PATH)
|
||||
|
||||
# Clear log
|
||||
with open(LOG_FILE, "w") as f:
|
||||
f.write("")
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(SOCKET_PATH)
|
||||
sock.listen(5)
|
||||
|
||||
# Ensure the socket is world-readable/writable
|
||||
os.chmod(SOCKET_PATH, 0o777)
|
||||
|
||||
log(f"Fake Discord IPC listening on: {SOCKET_PATH}")
|
||||
log("Waiting for RPC client connections...")
|
||||
|
||||
try:
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
|
||||
t.start()
|
||||
except KeyboardInterrupt:
|
||||
log("Shutting down...")
|
||||
finally:
|
||||
sock.close()
|
||||
if os.path.exists(SOCKET_PATH):
|
||||
os.unlink(SOCKET_PATH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fake Minecraft Server for testing MCC.
|
||||
Handles:
|
||||
- Server List Ping (status)
|
||||
- Offline-mode Login (no encryption)
|
||||
- Completes login phase but does NOT send JoinGame
|
||||
- Sends keep-alive packets to maintain connection
|
||||
Uses protocol 763 (Minecraft 1.20.1) to avoid the Configuration phase.
|
||||
|
||||
The key insight: MCC's DiscordRpc bot initializes during the login phase,
|
||||
before JoinGame is processed. We keep the connection open so the async
|
||||
RPC send can complete without needing complex NBT registry data.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 25565
|
||||
PROTOCOL_VERSION = 763 # 1.20.1
|
||||
MC_VERSION = "1.20.1"
|
||||
LOG_FILE = "/tmp/e2e-test/fake_mc_server.log"
|
||||
|
||||
client_joined = threading.Event()
|
||||
|
||||
|
||||
def log(msg):
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
line = f"[{timestamp}] [FakeMCServer] {msg}"
|
||||
print(line, flush=True)
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
# -- VarInt encoding/decoding --
|
||||
|
||||
def write_varint(value):
|
||||
"""Encode a VarInt."""
|
||||
result = bytearray()
|
||||
value &= 0xFFFFFFFF # handle negative values
|
||||
while True:
|
||||
byte = value & 0x7F
|
||||
value >>= 7
|
||||
if value != 0:
|
||||
byte |= 0x80
|
||||
result.append(byte)
|
||||
if value == 0:
|
||||
break
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def read_varint(data, offset=0):
|
||||
"""Decode a VarInt, return (value, new_offset)."""
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
byte = data[offset]
|
||||
offset += 1
|
||||
result |= (byte & 0x7F) << shift
|
||||
if (byte & 0x80) == 0:
|
||||
break
|
||||
shift += 7
|
||||
if shift >= 35:
|
||||
raise ValueError("VarInt too large")
|
||||
# Sign extension for negative values
|
||||
if result & (1 << 31):
|
||||
result -= 1 << 32
|
||||
return result, offset
|
||||
|
||||
|
||||
def read_varint_from_socket(sock):
|
||||
"""Read a VarInt from socket one byte at a time."""
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
byte_data = sock.recv(1)
|
||||
if not byte_data:
|
||||
raise ConnectionError("Connection closed")
|
||||
byte = byte_data[0]
|
||||
result |= (byte & 0x7F) << shift
|
||||
if (byte & 0x80) == 0:
|
||||
break
|
||||
shift += 7
|
||||
if shift >= 35:
|
||||
raise ValueError("VarInt too large")
|
||||
if result & (1 << 31):
|
||||
result -= 1 << 32
|
||||
return result
|
||||
|
||||
|
||||
def write_string(s):
|
||||
"""Encode a Minecraft string (VarInt length + UTF-8 bytes)."""
|
||||
encoded = s.encode("utf-8")
|
||||
return write_varint(len(encoded)) + encoded
|
||||
|
||||
|
||||
def read_string(data, offset):
|
||||
"""Decode a Minecraft string."""
|
||||
length, offset = read_varint(data, offset)
|
||||
s = data[offset:offset + length].decode("utf-8")
|
||||
return s, offset + length
|
||||
|
||||
|
||||
def make_packet(packet_id, payload=b""):
|
||||
"""Wrap data into a Minecraft packet: [length VarInt][packet_id VarInt][payload]."""
|
||||
pid = write_varint(packet_id)
|
||||
packet_data = pid + payload
|
||||
return write_varint(len(packet_data)) + packet_data
|
||||
|
||||
|
||||
def read_packet(sock):
|
||||
"""Read a full Minecraft packet from socket. Returns (packet_id, payload_bytes)."""
|
||||
length = read_varint_from_socket(sock)
|
||||
if length <= 0:
|
||||
return None, b""
|
||||
data = b""
|
||||
while len(data) < length:
|
||||
chunk = sock.recv(length - len(data))
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed mid-packet")
|
||||
data += chunk
|
||||
packet_id, offset = read_varint(data, 0)
|
||||
return packet_id, data[offset:]
|
||||
|
||||
|
||||
# -- Packet builders --
|
||||
|
||||
def build_status_response():
|
||||
"""Build Status Response (0x00) packet."""
|
||||
status = {
|
||||
"version": {"name": MC_VERSION, "protocol": PROTOCOL_VERSION},
|
||||
"players": {"max": 20, "online": 1, "sample": []},
|
||||
"description": {"text": "MCC Discord RPC Test Server"},
|
||||
"enforcesSecureChat": False,
|
||||
"previewsChat": False
|
||||
}
|
||||
return make_packet(0x00, write_string(json.dumps(status)))
|
||||
|
||||
|
||||
def build_ping_response(payload_bytes):
|
||||
"""Build Ping Response (0x01) packet."""
|
||||
return make_packet(0x01, payload_bytes)
|
||||
|
||||
|
||||
def build_login_success(username):
|
||||
"""Build Login Success (0x02) packet for 1.20.1."""
|
||||
player_uuid = uuid.uuid3(uuid.NAMESPACE_DNS, f"OfflinePlayer:{username}")
|
||||
uuid_bytes = player_uuid.bytes
|
||||
name_bytes = write_string(username)
|
||||
num_properties = write_varint(0) # No properties
|
||||
return make_packet(0x02, uuid_bytes + name_bytes + num_properties)
|
||||
|
||||
|
||||
def build_keep_alive(keep_alive_id=0):
|
||||
"""Build Keep Alive (0x24 for 1.20.1) packet."""
|
||||
return make_packet(0x24, struct.pack(">q", keep_alive_id))
|
||||
|
||||
|
||||
def handle_client(conn, addr):
|
||||
"""Handle a single MCC client connection."""
|
||||
log(f"Client connected from {addr}")
|
||||
state = "handshake" # handshake -> status or login -> play
|
||||
username = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
packet_id, payload = read_packet(conn)
|
||||
if packet_id is None:
|
||||
break
|
||||
|
||||
if state == "handshake":
|
||||
if packet_id == 0x00:
|
||||
# Handshake packet
|
||||
proto, off = read_varint(payload, 0)
|
||||
host, off = read_string(payload, off)
|
||||
port = struct.unpack(">H", payload[off:off+2])[0]
|
||||
off += 2
|
||||
next_state, off = read_varint(payload, off)
|
||||
log(f"Handshake: protocol={proto}, host={host}, port={port}, next_state={next_state}")
|
||||
|
||||
if next_state == 1:
|
||||
state = "status"
|
||||
elif next_state == 2:
|
||||
state = "login"
|
||||
|
||||
elif state == "status":
|
||||
if packet_id == 0x00:
|
||||
# Status Request
|
||||
log("Status Request received, sending response")
|
||||
conn.sendall(build_status_response())
|
||||
elif packet_id == 0x01:
|
||||
# Ping
|
||||
log("Ping received, sending Pong")
|
||||
conn.sendall(build_ping_response(payload))
|
||||
break # Status connection is done
|
||||
|
||||
elif state == "login":
|
||||
if packet_id == 0x00:
|
||||
# Login Start
|
||||
username, off = read_string(payload, 0)
|
||||
log(f"Login Start: username={username}")
|
||||
|
||||
# Send Login Success (offline mode - no encryption)
|
||||
log(f"Sending Login Success for {username}")
|
||||
conn.sendall(build_login_success(username))
|
||||
|
||||
state = "play"
|
||||
|
||||
# Signal that client joined (login phase complete)
|
||||
client_joined.set()
|
||||
log(f"*** {username} has logged in successfully! ***")
|
||||
|
||||
# Keep the connection alive without sending JoinGame
|
||||
# MCC's DiscordRpc bot initializes during the login phase,
|
||||
# before JoinGame is processed. We keep the connection open
|
||||
# so the async RPC send can complete.
|
||||
log("Keeping connection alive (not sending JoinGame to avoid NBT complexity)...")
|
||||
|
||||
# Start keep-alive loop
|
||||
ka_thread = threading.Thread(
|
||||
target=keep_alive_loop, args=(conn,), daemon=True
|
||||
)
|
||||
ka_thread.start()
|
||||
|
||||
elif state == "play":
|
||||
# Just absorb play-state packets from the client silently
|
||||
pass
|
||||
|
||||
except (ConnectionError, ConnectionResetError, BrokenPipeError) as e:
|
||||
log(f"Client disconnected: {e}")
|
||||
except Exception as e:
|
||||
log(f"Error: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
if username:
|
||||
log(f"{username} disconnected")
|
||||
|
||||
|
||||
def keep_alive_loop(conn):
|
||||
"""Send keep-alive packets every 10 seconds."""
|
||||
ka_id = 0
|
||||
try:
|
||||
while True:
|
||||
time.sleep(10)
|
||||
ka_id += 1
|
||||
conn.sendall(build_keep_alive(ka_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
with open(LOG_FILE, "w") as f:
|
||||
f.write("")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((HOST, PORT))
|
||||
sock.listen(5)
|
||||
|
||||
log(f"Fake MC Server listening on {HOST}:{PORT} (protocol {PROTOCOL_VERSION}, {MC_VERSION})")
|
||||
log("Waiting for MCC connections...")
|
||||
|
||||
try:
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
|
||||
t.start()
|
||||
except KeyboardInterrupt:
|
||||
log("Shutting down...")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
End-to-end test orchestrator for Discord RPC ChatBot.
|
||||
Starts all components, waits for MCC to connect, and verifies RPC presence is sent.
|
||||
"""
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
MCC_DIR = "/home/runner/work/Minecraft-Console-Client/Minecraft-Console-Client"
|
||||
TEST_DIR = "/tmp/e2e-test"
|
||||
MC_LOG = f"{TEST_DIR}/fake_mc_server.log"
|
||||
RPC_LOG = f"{TEST_DIR}/discord_rpc.log"
|
||||
MCC_LOG = f"{TEST_DIR}/mcc_output.log"
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"\033[1;36m[E2E-TEST]\033[0m {msg}", flush=True)
|
||||
|
||||
|
||||
def wait_for_log(log_file, marker, timeout=30, label=""):
|
||||
"""Wait for a specific string to appear in a log file."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
content = f.read()
|
||||
if marker in content:
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
log(f"TIMEOUT waiting for '{marker}' in {label or log_file}")
|
||||
return False
|
||||
|
||||
|
||||
def read_log(log_file):
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(TEST_DIR, exist_ok=True)
|
||||
|
||||
pids = []
|
||||
|
||||
# Clean up old files
|
||||
for f in [MC_LOG, RPC_LOG, MCC_LOG]:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
|
||||
# Remove any leftover mcc_input.txt
|
||||
input_file = os.path.join(MCC_DIR, "mcc_input.txt")
|
||||
if os.path.exists(input_file):
|
||||
os.remove(input_file)
|
||||
|
||||
try:
|
||||
# -- Step 1: Start fake Discord IPC --
|
||||
log("Starting fake Discord IPC server...")
|
||||
discord_proc = subprocess.Popen(
|
||||
[sys.executable, f"{TEST_DIR}/fake_discord_ipc.py"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
env={**os.environ, "XDG_RUNTIME_DIR": "/tmp"}
|
||||
)
|
||||
pids.append(discord_proc.pid)
|
||||
time.sleep(1)
|
||||
|
||||
if not wait_for_log(RPC_LOG, "Fake Discord IPC listening", timeout=5, label="Discord IPC"):
|
||||
log("FAIL: Discord IPC server did not start")
|
||||
return 1
|
||||
log(" OK: Discord IPC server running")
|
||||
|
||||
# -- Step 2: Start fake Minecraft server --
|
||||
log("Starting fake Minecraft server...")
|
||||
mc_proc = subprocess.Popen(
|
||||
[sys.executable, f"{TEST_DIR}/fake_mc_server.py"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
pids.append(mc_proc.pid)
|
||||
time.sleep(1)
|
||||
|
||||
if not wait_for_log(MC_LOG, "Fake MC Server listening", timeout=5, label="MC Server"):
|
||||
log("FAIL: MC server did not start")
|
||||
return 1
|
||||
log(" OK: Minecraft server running on 127.0.0.1:25565")
|
||||
|
||||
# -- Step 3: Create MCC configuration --
|
||||
log("Creating MCC configuration...")
|
||||
ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
|
||||
with open(ini_path, "w") as f:
|
||||
f.write("""
|
||||
[Main]
|
||||
[Main.General]
|
||||
Account = { Login = "RpcTestBot", Password = "-" }
|
||||
ServerIP = "127.0.0.1:25565"
|
||||
MinecraftVersion = "1.20.1"
|
||||
|
||||
[Main.Advanced]
|
||||
Language = "en_us"
|
||||
EnableSentry = false
|
||||
|
||||
[Logging]
|
||||
DebugMessages = true
|
||||
|
||||
[ChatBot]
|
||||
[ChatBot.DiscordRpc]
|
||||
Enabled = true
|
||||
ApplicationId = "123456789012345678"
|
||||
PresenceDetails = "Playing on {server_host}:{server_port}"
|
||||
PresenceState = "{dimension} - HP: {health}/{max_health}"
|
||||
LargeImageKey = "mcc_icon"
|
||||
LargeImageText = "Minecraft Console Client"
|
||||
SmallImageKey = ""
|
||||
SmallImageText = ""
|
||||
ShowServerAddress = true
|
||||
ShowCoordinates = true
|
||||
ShowHealth = true
|
||||
ShowDimension = true
|
||||
ShowGamemode = true
|
||||
ShowElapsedTime = true
|
||||
ShowPlayerCount = true
|
||||
UpdateIntervalSeconds = 5
|
||||
""")
|
||||
log(" OK: MinecraftClient.ini created")
|
||||
|
||||
# -- Step 4: Start MCC --
|
||||
log("Starting MCC...")
|
||||
mcc_env = {
|
||||
**os.environ,
|
||||
"MCC_FILE_INPUT": "1",
|
||||
"XDG_RUNTIME_DIR": "/tmp" # So DiscordRPC library finds our fake IPC socket
|
||||
}
|
||||
|
||||
mcc_proc = subprocess.Popen(
|
||||
["dotnet", "run", "--project", "MinecraftClient", "-c", "Release",
|
||||
"--no-build", "--", "RpcTestBot", "-", "127.0.0.1:25565"],
|
||||
cwd=MCC_DIR,
|
||||
stdout=open(MCC_LOG, "w"),
|
||||
stderr=subprocess.STDOUT,
|
||||
env=mcc_env
|
||||
)
|
||||
pids.append(mcc_proc.pid)
|
||||
|
||||
# -- Step 5: Wait for MCC to join the server --
|
||||
log("Waiting for MCC to connect to server...")
|
||||
mc_joined = wait_for_log(MC_LOG, "has logged in successfully", timeout=30, label="MC join")
|
||||
|
||||
if mc_joined:
|
||||
log(" OK: MCC connected to fake Minecraft server")
|
||||
else:
|
||||
log(" WARN: Could not confirm MC join in server log")
|
||||
# Check MCC log for more info
|
||||
mcc_content = read_log(MCC_LOG)
|
||||
if "Server was successfully joined" in mcc_content:
|
||||
log(" OK: MCC reports successful join in its own log")
|
||||
mc_joined = True
|
||||
|
||||
# -- Step 6: Wait for Discord RPC presence --
|
||||
log("Waiting for Discord RPC presence update...")
|
||||
# Give MCC time to initialize the Discord RPC bot and set presence
|
||||
rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=30, label="RPC activity")
|
||||
|
||||
if rpc_activity:
|
||||
log(" OK: Discord RPC presence received!")
|
||||
else:
|
||||
log(" INFO: Checking if RPC client attempted connection...")
|
||||
rpc_content = read_log(RPC_LOG)
|
||||
if "HANDSHAKE received" in rpc_content:
|
||||
log(" OK: RPC handshake succeeded, waiting longer for activity...")
|
||||
rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=20, label="RPC activity (extended)")
|
||||
elif "Client connected" in rpc_content:
|
||||
log(" PARTIAL: RPC client connected but no activity sent yet")
|
||||
|
||||
# -- Step 7: Collect and display results --
|
||||
log("")
|
||||
log("=" * 60)
|
||||
log("END-TO-END TEST RESULTS")
|
||||
log("=" * 60)
|
||||
|
||||
# Check all criteria
|
||||
mc_server_ok = "Fake MC Server listening" in read_log(MC_LOG)
|
||||
discord_ipc_ok = "Fake Discord IPC listening" in read_log(RPC_LOG)
|
||||
mcc_content = read_log(MCC_LOG)
|
||||
rpc_content = read_log(RPC_LOG)
|
||||
|
||||
mcc_connected = "has logged in successfully" in read_log(MC_LOG) or "Server was successfully joined" in mcc_content
|
||||
rpc_handshake = "HANDSHAKE received" in rpc_content
|
||||
rpc_ready = "Sent READY response" in rpc_content
|
||||
rpc_presence = "SET_ACTIVITY received" in rpc_content
|
||||
|
||||
# Extract presence details from RPC log
|
||||
presence_details = ""
|
||||
presence_state = ""
|
||||
for line in rpc_content.split("\n"):
|
||||
if "Details :" in line:
|
||||
presence_details = line.split("Details :")[1].strip()
|
||||
if "State :" in line:
|
||||
presence_state = line.split("State :")[1].strip()
|
||||
|
||||
results = [
|
||||
("Fake MC Server started", mc_server_ok),
|
||||
("Fake Discord IPC started", discord_ipc_ok),
|
||||
("MCC connected to server", mcc_connected),
|
||||
("RPC handshake completed", rpc_handshake),
|
||||
("RPC READY sent to client", rpc_ready),
|
||||
("RPC presence set", rpc_presence),
|
||||
]
|
||||
|
||||
all_pass = True
|
||||
for label, ok in results:
|
||||
status = "\033[1;32mPASS\033[0m" if ok else "\033[1;31mFAIL\033[0m"
|
||||
log(f" [{status}] {label}")
|
||||
if not ok:
|
||||
all_pass = False
|
||||
|
||||
if presence_details:
|
||||
log(f"\n Presence Details: {presence_details}")
|
||||
if presence_state:
|
||||
log(f" Presence State : {presence_state}")
|
||||
|
||||
log("")
|
||||
|
||||
# Print relevant logs
|
||||
log("--- MCC Output (last 30 lines) ---")
|
||||
mcc_lines = mcc_content.strip().split("\n")
|
||||
for line in mcc_lines[-30:]:
|
||||
log(f" {line}")
|
||||
|
||||
log("")
|
||||
log("--- Discord RPC Log ---")
|
||||
rpc_lines = rpc_content.strip().split("\n")
|
||||
for line in rpc_lines:
|
||||
log(f" {line}")
|
||||
|
||||
log("")
|
||||
log("--- MC Server Log ---")
|
||||
mc_content = read_log(MC_LOG)
|
||||
mc_lines = mc_content.strip().split("\n")
|
||||
for line in mc_lines:
|
||||
log(f" {line}")
|
||||
|
||||
log("")
|
||||
|
||||
if all_pass:
|
||||
log("\033[1;32m*** ALL TESTS PASSED - Discord RPC integration is fully working! ***\033[0m")
|
||||
return 0
|
||||
else:
|
||||
log("\033[1;33m*** SOME TESTS DID NOT PASS ***\033[0m")
|
||||
return 1
|
||||
|
||||
finally:
|
||||
# Clean up all processes
|
||||
log("\nCleaning up processes...")
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
# Clean up config
|
||||
ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
|
||||
if os.path.exists(ini_path):
|
||||
os.remove(ini_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -381,7 +381,7 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
string message = e.Message.Content.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message))
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return;
|
||||
|
||||
// Relay messages from other bots when configured, but never process commands from them.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using DiscordRPC;
|
||||
using DiscordRPC.Logging;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -88,11 +87,11 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
private DiscordRpcClient? rpcClient;
|
||||
private int tickCounter;
|
||||
private int updateIntervalTicks;
|
||||
private Timestamps? sessionTimestamps;
|
||||
private float lastHealth;
|
||||
private DiscordRpcClient? _rpcClient;
|
||||
private int _tickCounter;
|
||||
private int _updateIntervalTicks;
|
||||
private Timestamps? _sessionTimestamps;
|
||||
private float _lastHealth;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -105,32 +104,26 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
try
|
||||
{
|
||||
rpcClient = new DiscordRpcClient(Config.ApplicationId.Trim())
|
||||
_rpcClient = new DiscordRpcClient(Config.ApplicationId.Trim())
|
||||
{
|
||||
Logger = Settings.Config.Logging.DebugMessages
|
||||
? new ConsoleLogger(LogLevel.Trace)
|
||||
: new ConsoleLogger(LogLevel.None)
|
||||
};
|
||||
|
||||
rpcClient.OnReady += (_, e) =>
|
||||
_rpcClient.OnReady += (_, e) =>
|
||||
{
|
||||
LogToConsole(string.Format(Translations.bot_DiscordRpc_connected, e.User.Username));
|
||||
};
|
||||
|
||||
rpcClient.OnConnectionFailed += (_, e) =>
|
||||
_rpcClient.OnConnectionFailed += (_, e) =>
|
||||
{
|
||||
LogToConsole(string.Format(Translations.bot_DiscordRpc_connection_failed, e.FailedPipe));
|
||||
};
|
||||
|
||||
rpcClient.Initialize();
|
||||
updateIntervalTicks = Settings.DoubleToTick(Config.UpdateIntervalSeconds);
|
||||
_rpcClient.Initialize();
|
||||
_updateIntervalTicks = Settings.DoubleToTick(Config.UpdateIntervalSeconds);
|
||||
|
||||
if (Config.ShowElapsedTime)
|
||||
sessionTimestamps = Timestamps.Now;
|
||||
|
||||
lastHealth = Handler.GetHealth();
|
||||
|
||||
SetPresence();
|
||||
LogToConsole(Translations.bot_DiscordRpc_initialized);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -143,49 +136,51 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
public override void OnUnload()
|
||||
{
|
||||
if (rpcClient is { IsDisposed: false })
|
||||
if (_rpcClient is { IsDisposed: false })
|
||||
{
|
||||
rpcClient.ClearPresence();
|
||||
rpcClient.Dispose();
|
||||
_rpcClient.ClearPresence();
|
||||
_rpcClient.Dispose();
|
||||
}
|
||||
|
||||
rpcClient = null;
|
||||
_rpcClient = null;
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
if (Config.ShowElapsedTime)
|
||||
sessionTimestamps = Timestamps.Now;
|
||||
_sessionTimestamps = Timestamps.Now;
|
||||
|
||||
_lastHealth = Handler.GetHealth();
|
||||
_tickCounter = 0;
|
||||
SetPresence();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
tickCounter++;
|
||||
if (tickCounter < updateIntervalTicks)
|
||||
_tickCounter++;
|
||||
if (_tickCounter < _updateIntervalTicks)
|
||||
return;
|
||||
|
||||
tickCounter = 0;
|
||||
_tickCounter = 0;
|
||||
SetPresence();
|
||||
}
|
||||
|
||||
public override void OnHealthUpdate(float health, int food)
|
||||
{
|
||||
lastHealth = health;
|
||||
_lastHealth = health;
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
if (rpcClient is { IsDisposed: false })
|
||||
rpcClient.ClearPresence();
|
||||
if (_rpcClient is { IsDisposed: false })
|
||||
_rpcClient.ClearPresence();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetPresence()
|
||||
{
|
||||
if (rpcClient is null or { IsDisposed: true })
|
||||
if (_rpcClient is null or { IsDisposed: true })
|
||||
return;
|
||||
|
||||
try
|
||||
|
|
@ -223,8 +218,8 @@ namespace MinecraftClient.ChatBots
|
|||
presence.Assets = assets;
|
||||
|
||||
// Timestamps
|
||||
if (Config.ShowElapsedTime && sessionTimestamps is not null)
|
||||
presence.Timestamps = sessionTimestamps;
|
||||
if (Config.ShowElapsedTime && _sessionTimestamps is not null)
|
||||
presence.Timestamps = _sessionTimestamps;
|
||||
|
||||
// Player count as party
|
||||
if (Config.ShowPlayerCount)
|
||||
|
|
@ -242,7 +237,7 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
rpcClient.SetPresence(presence);
|
||||
_rpcClient.SetPresence(presence);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -282,7 +277,7 @@ namespace MinecraftClient.ChatBots
|
|||
dimensionName = dim.Name ?? "Unknown";
|
||||
|
||||
// Clean up the dimension name for display
|
||||
if (dimensionName.StartsWith("minecraft:"))
|
||||
if (dimensionName.StartsWith("minecraft:", StringComparison.Ordinal))
|
||||
dimensionName = dimensionName["minecraft:".Length..];
|
||||
|
||||
dimensionName = dimensionName switch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue