159 lines
5.8 KiB
Python
159 lines
5.8 KiB
Python
import json
|
|
import sqlite3
|
|
from fastapi import FastAPI, HTTPException, status, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from database import init_db, get_db
|
|
from models import RegisterRequest, LoginRequest, AuthResponse, SaveGameRequest, SyncTickRequest
|
|
from auth import hash_password, verify_password, create_access_token, get_current_user_id
|
|
import engine
|
|
|
|
app = FastAPI(title="Production API", version="2.0.0")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
@app.on_event("startup")
|
|
def startup_event():
|
|
init_db()
|
|
|
|
@app.get("/api/health")
|
|
def health_check():
|
|
return {"status": "ok", "app": "Production - The Idle Game", "version": "2.0.0"}
|
|
|
|
@app.post("/api/auth/register", response_model=AuthResponse)
|
|
def register(req: RegisterRequest):
|
|
if req.website_hp and req.website_hp.strip():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Registration verification failed."
|
|
)
|
|
|
|
clean_username = req.username.strip()
|
|
if not clean_username:
|
|
raise HTTPException(status_code=400, detail="Username cannot be blank.")
|
|
|
|
hashed = hash_password(req.password)
|
|
|
|
try:
|
|
with get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
|
|
(clean_username, hashed)
|
|
)
|
|
user_id = cursor.lastrowid
|
|
|
|
# Initialize clean server authoritative game state
|
|
init_state = engine.get_initial_state()
|
|
cursor.execute(
|
|
"INSERT INTO saves (user_id, save_data, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
|
|
(user_id, json.dumps(init_state))
|
|
)
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Username already taken."
|
|
)
|
|
|
|
token = create_access_token({"sub": str(user_id), "username": clean_username})
|
|
return AuthResponse(token=token, username=clean_username)
|
|
|
|
@app.post("/api/auth/login", response_model=AuthResponse)
|
|
def login(req: LoginRequest):
|
|
clean_username = req.username.strip()
|
|
with get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"SELECT id, username, password_hash FROM users WHERE username = ?",
|
|
(clean_username,)
|
|
)
|
|
row = cursor.fetchone()
|
|
|
|
if not row or not verify_password(req.password, row["password_hash"]):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid username or password."
|
|
)
|
|
|
|
user_id = row["id"]
|
|
token = create_access_token({"sub": str(user_id), "username": clean_username})
|
|
return AuthResponse(token=token, username=clean_username)
|
|
|
|
@app.get("/api/game/state")
|
|
def get_game_state(user_id: int = Depends(get_current_user_id)):
|
|
with get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT save_data, updated_at FROM saves WHERE user_id = ?", (user_id,))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
init_state = engine.get_initial_state()
|
|
return {"has_save": True, "save_data": init_state, "updated_at": None}
|
|
|
|
try:
|
|
data = json.loads(row["save_data"])
|
|
# Process authoritative offline progress up to now
|
|
data = engine.process_offline_and_ticks(data)
|
|
return {"has_save": True, "save_data": data, "updated_at": row["updated_at"]}
|
|
except Exception:
|
|
return {"has_save": True, "save_data": engine.get_initial_state(), "updated_at": None}
|
|
|
|
@app.post("/api/game/tick")
|
|
def sync_tick(req: SyncTickRequest, user_id: int = Depends(get_current_user_id)):
|
|
"""Authoritative periodic tick (called every 5-10s): applies queued actions and computes tick progress."""
|
|
with get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT save_data FROM saves WHERE user_id = ?", (user_id,))
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
try:
|
|
state = json.loads(row["save_data"])
|
|
except Exception:
|
|
state = engine.get_initial_state()
|
|
else:
|
|
state = engine.get_initial_state()
|
|
|
|
# 1. Advance production, automation, and offline ticks up to right now
|
|
# Guarantees all idle income earned up to this exact second is credited before actions
|
|
state = engine.process_offline_and_ticks(state)
|
|
|
|
# 2. Apply authorized player actions with fresh resource balances
|
|
if req.actions:
|
|
state = engine.apply_actions(state, req.actions)
|
|
|
|
# 3. Persist authoritative clean state
|
|
cursor.execute("""
|
|
INSERT INTO saves (user_id, save_data, updated_at)
|
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
save_data = excluded.save_data,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
""", (user_id, json.dumps(state)))
|
|
conn.commit()
|
|
|
|
return {"status": "ok", "save_data": state, "ack_seq": req.seq}
|
|
|
|
@app.post("/api/game/save")
|
|
def save_game_state(req: SaveGameRequest, user_id: int = Depends(get_current_user_id)):
|
|
# Sanitize data before persisting
|
|
clean_state = engine.sanitize_state(req.save_data)
|
|
json_data = json.dumps(clean_state)
|
|
with get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT INTO saves (user_id, save_data, updated_at)
|
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
save_data = excluded.save_data,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
""", (user_id, json_data))
|
|
conn.commit()
|
|
|
|
return {"status": "saved", "save_data": clean_state}
|