Skip to content

Instantly share code, notes, and snippets.

@GrenderG
Last active April 16, 2025 20:55
Show Gist options
  • Save GrenderG/ee304cb8ebdc94f87e8598b448c39452 to your computer and use it in GitHub Desktop.
Save GrenderG/ee304cb8ebdc94f87e8598b448c39452 to your computer and use it in GitHub Desktop.
Nose Hair Master 2 i-mode game replacement server.
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