Last active
April 16, 2025 20:55
-
-
Save GrenderG/ee304cb8ebdc94f87e8598b448c39452 to your computer and use it in GitHub Desktop.
Nose Hair Master 2 i-mode game replacement server.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from flask import Flask, request, make_response | |
import struct | |
import json | |
import os | |
app = Flask(__name__) | |
# Configuration. | |
LEADERBOARD_FILE = 'leaderboard.json' | |
FF0A = chr(65290) # Fullwidth asterisk (U+FF0A). | |
PORT = 80 | |
# Initialize with dummy data if file doesn't exist. | |
if not os.path.exists(LEADERBOARD_FILE): | |
dummy_data = {'NULLGWDOCOMO': 4000} | |
with open(LEADERBOARD_FILE, 'w') as f: | |
json.dump(dummy_data, f) | |
def load_leaderboard(): | |
"""Load leaderboard data from JSON file.""" | |
with open(LEADERBOARD_FILE, 'r') as f: | |
return json.load(f) | |
def save_leaderboard(data): | |
"""Save leaderboard data to JSON file.""" | |
with open(LEADERBOARD_FILE, 'w') as f: | |
json.dump(data, f) | |
def format_time(centiseconds): | |
"""Format centiseconds to seconds with two decimal places.""" | |
return f'{centiseconds//100}.{centiseconds%100:02d}' | |
def get_rank(time_centisec, leaderboard): | |
"""Calculate 1-based rank (lower time = better).""" | |
sorted_times = sorted(leaderboard.values()) | |
try: | |
return sorted_times.index(time_centisec) + 1 | |
except ValueError: | |
return len(sorted_times) + 1 | |
def create_java_utf8_string(input_string): | |
"""Create byte sequence readable by Java's DataInputStream.readUTF(). | |
Args: | |
input_string: The string to be converted to Java's modified UTF-8 format. | |
Returns: | |
bytes: The byte sequence that can be read by readUTF(). | |
""" | |
utf8_bytes = input_string.encode('utf-8') | |
length = len(utf8_bytes) | |
# Java's readUTF expects a 16-bit length prefix (unsigned short, big-endian). | |
length_bytes = struct.pack('>H', length) | |
return length_bytes + utf8_bytes | |
@app.route('/bakaservlet', methods=['POST']) | |
def handle_leaderboard(): | |
"""Handle leaderboard requests.""" | |
# Get uid from form data. | |
uid = request.args.get('uid') | |
if not uid: | |
print('Missing uid, defaulting to NULLGWDOCOMO.') | |
uid = 'NULLGWDOCOMO' | |
try: | |
data = request.data | |
unk1 = struct.unpack('>i', data[0:4])[0] | |
mode = struct.unpack('>i', data[4:8])[0] | |
time_centisec = struct.unpack('>i', data[8:12])[0] | |
except Exception: | |
return make_response(struct.pack('>i', 0)) # Error response. | |
leaderboard = load_leaderboard() | |
if mode == 2: # Rank check? | |
return handle_rank_check(uid, leaderboard) | |
elif mode == 3: # Score submission. | |
return handle_score_submission(uid, time_centisec, leaderboard) | |
else: | |
return make_response(struct.pack('>i', 0)) # Unsupported mode. | |
def handle_rank_check(uid, leaderboard): | |
"""Handle mode 2: Rank check? (MISSION COMPLETE!).""" | |
# Start building response. | |
response_text = f'MISSION COMPLETE!{FF0A}' | |
# Send player's ranking if available. | |
if uid not in leaderboard: | |
leaderboard[uid] = current_time | |
response_text += 'Rank not found\n' | |
else: | |
response_text += f'Best time: {format_time(leaderboard[uid])}\n' | |
response_text += f'Current rank: {get_rank(leaderboard[uid], leaderboard)}' | |
# Convert to UTF-8 bytes. | |
response = create_java_utf8_string(response_text) | |
# Below ints seem to be stored in the .sp file. | |
# See comment in `handle_score_submission` for explanation. | |
response += struct.pack('>I', 0) | |
response += struct.pack('>I', 0) | |
resp = make_response(response) | |
resp.headers['Content-Type'] = 'application/octet-stream' | |
return resp | |
def handle_score_submission(uid, current_time, leaderboard): | |
"""Handle mode 3: Score submission (RANKING).""" | |
# Start building response. | |
response_text = f'RANKING{FF0A}' | |
# Add player's info if available. | |
if uid not in leaderboard or current_time < leaderboard[uid]: | |
leaderboard[uid] = current_time | |
response_text += 'New personal record!\n' | |
response_text += f'Current rank: {get_rank(current_time, leaderboard)}' | |
save_leaderboard(leaderboard) | |
else: | |
response_text += f'Best time: {format_time(leaderboard[uid])}\n' | |
response_text += f'Current rank: {get_rank(current_time, leaderboard)}' | |
# Convert to UTF-8 bytes. | |
response = create_java_utf8_string(response_text) | |
# These integers appear to be stored in the .sp file. They likely control rank submission status, | |
# with the first integer hiding the ranking option when set to 1. Defaulting to 0 since the | |
# exact behavior isn't fully understood yet. | |
response += struct.pack('>I', 0) | |
response += struct.pack('>I', 0) | |
resp = make_response(response) | |
resp.headers['Content-Type'] = 'application/octet-stream' | |
return resp | |
if __name__ == '__main__': | |
app.run(host='127.0.0.1', port=PORT) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment