Commit ·
820bd65
1
Parent(s): 78d6422
feat: per-user data isolation, fix model name, clean gitignore
Browse files- core/session.py: ContextVar-based user isolation for multi-user HF Spaces
- Player/World data stored per-user in data/users/{hash}/
- Session ID generated from request headers or random
- Key handlers (dream, explore, rest, quest) set user context
- Fix hero tag: MiniCPM5-1B -> MiniCPM4.1-8B (matches config)
- Update .gitignore: exclude data/saves/, player.json, active_slot.txt
- Fix test_player.py: update mock patches for new session functions
- .gitignore +3 -0
- core/player.py +20 -16
- core/session.py +71 -0
- tests/test_player.py +7 -7
- ui/main.py +28 -11
.gitignore
CHANGED
|
@@ -21,6 +21,9 @@ env/
|
|
| 21 |
# Data (runtime-generated)
|
| 22 |
data/world.json
|
| 23 |
data/dreams.json
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# OS
|
| 26 |
.DS_Store
|
|
|
|
| 21 |
# Data (runtime-generated)
|
| 22 |
data/world.json
|
| 23 |
data/dreams.json
|
| 24 |
+
data/saves/
|
| 25 |
+
data/player.json
|
| 26 |
+
data/active_slot.txt
|
| 27 |
|
| 28 |
# OS
|
| 29 |
.DS_Store
|
core/player.py
CHANGED
|
@@ -183,55 +183,59 @@ def save_player(player: Player):
|
|
| 183 |
|
| 184 |
|
| 185 |
# ============================================================
|
| 186 |
-
# Save Slot System —
|
| 187 |
# ============================================================
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
|
| 193 |
def _slot_path(slot: int) -> Path:
|
| 194 |
-
""
|
| 195 |
-
return SLOTS_DIR / f"slot_{slot}" / "player.json"
|
| 196 |
|
| 197 |
|
| 198 |
def get_slot_dir(slot: int = None) -> Path:
|
| 199 |
-
"""Return the directory for a save slot (creates if needed)."""
|
| 200 |
if slot is None:
|
| 201 |
slot = get_active_slot()
|
| 202 |
-
d =
|
| 203 |
d.mkdir(parents=True, exist_ok=True)
|
| 204 |
return d
|
| 205 |
|
| 206 |
|
| 207 |
def get_current_world_file() -> Path:
|
| 208 |
-
"""Return the world.json path for the active save slot."""
|
| 209 |
return get_slot_dir() / "world.json"
|
| 210 |
|
| 211 |
|
| 212 |
def get_current_dreams_file() -> Path:
|
| 213 |
-
"""Return the dreams.json path for the active save slot."""
|
| 214 |
return get_slot_dir() / "dreams.json"
|
| 215 |
|
| 216 |
|
| 217 |
def _slot_meta_path() -> Path:
|
| 218 |
-
return
|
| 219 |
|
| 220 |
|
| 221 |
def get_active_slot() -> int:
|
| 222 |
-
|
| 223 |
-
if
|
| 224 |
try:
|
| 225 |
-
return int(
|
| 226 |
except (ValueError, TypeError):
|
| 227 |
pass
|
| 228 |
return 0
|
| 229 |
|
| 230 |
|
| 231 |
def set_active_slot(slot: int):
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
|
| 236 |
|
| 237 |
def _update_slot_meta(slot: int, player: Player):
|
|
|
|
| 183 |
|
| 184 |
|
| 185 |
# ============================================================
|
| 186 |
+
# Save Slot System — Per-user isolation via session
|
| 187 |
# ============================================================
|
| 188 |
|
| 189 |
+
def _get_slots_dir() -> Path:
|
| 190 |
+
"""Get the saves directory for the current user session."""
|
| 191 |
+
from core.session import get_user_saves_dir
|
| 192 |
+
return get_user_saves_dir()
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _get_active_slot_file() -> Path:
|
| 196 |
+
"""Get the active slot file for the current user session."""
|
| 197 |
+
from core.session import get_user_active_slot_file
|
| 198 |
+
return get_user_active_slot_file()
|
| 199 |
|
| 200 |
|
| 201 |
def _slot_path(slot: int) -> Path:
|
| 202 |
+
return _get_slots_dir() / f"slot_{slot}" / "player.json"
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
def get_slot_dir(slot: int = None) -> Path:
|
|
|
|
| 206 |
if slot is None:
|
| 207 |
slot = get_active_slot()
|
| 208 |
+
d = _get_slots_dir() / f"slot_{slot}"
|
| 209 |
d.mkdir(parents=True, exist_ok=True)
|
| 210 |
return d
|
| 211 |
|
| 212 |
|
| 213 |
def get_current_world_file() -> Path:
|
|
|
|
| 214 |
return get_slot_dir() / "world.json"
|
| 215 |
|
| 216 |
|
| 217 |
def get_current_dreams_file() -> Path:
|
|
|
|
| 218 |
return get_slot_dir() / "dreams.json"
|
| 219 |
|
| 220 |
|
| 221 |
def _slot_meta_path() -> Path:
|
| 222 |
+
return _get_slots_dir() / "slots_meta.json"
|
| 223 |
|
| 224 |
|
| 225 |
def get_active_slot() -> int:
|
| 226 |
+
path = _get_active_slot_file()
|
| 227 |
+
if path.exists():
|
| 228 |
try:
|
| 229 |
+
return int(path.read_text(encoding="utf-8").strip())
|
| 230 |
except (ValueError, TypeError):
|
| 231 |
pass
|
| 232 |
return 0
|
| 233 |
|
| 234 |
|
| 235 |
def set_active_slot(slot: int):
|
| 236 |
+
path = _get_active_slot_file()
|
| 237 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 238 |
+
path.write_text(str(slot), encoding="utf-8")
|
| 239 |
|
| 240 |
|
| 241 |
def _update_slot_meta(slot: int, player: Player):
|
core/session.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Session management — per-user data isolation for multi-user HF Spaces."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from contextvars import ContextVar
|
| 7 |
+
from functools import wraps
|
| 8 |
+
|
| 9 |
+
import config
|
| 10 |
+
|
| 11 |
+
USERS_DIR = config.DATA_DIR / "users"
|
| 12 |
+
_current_user_id: ContextVar[str] = ContextVar("current_user_id", default="default")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def set_current_user(user_id: str):
|
| 16 |
+
_current_user_id.set(user_id)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_current_user_id() -> str:
|
| 20 |
+
return _current_user_id.get()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def with_user(fn):
|
| 24 |
+
"""Decorator: extracts session_id from inputs and sets user context."""
|
| 25 |
+
@wraps(fn)
|
| 26 |
+
def wrapper(*args, **kwargs):
|
| 27 |
+
# Find session_id in args (usually last positional arg)
|
| 28 |
+
sid = "default"
|
| 29 |
+
for a in reversed(args):
|
| 30 |
+
if isinstance(a, str) and len(a) == 16 and all(c in '0123456789abcdef' for c in a):
|
| 31 |
+
sid = a
|
| 32 |
+
break
|
| 33 |
+
set_current_user(sid)
|
| 34 |
+
# Remove session_id from args for the actual function
|
| 35 |
+
new_args = [a for a in args if a is not sid]
|
| 36 |
+
return fn(*new_args, **kwargs)
|
| 37 |
+
return wrapper
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_user_dir(user_id: str = None) -> Path:
|
| 41 |
+
uid = user_id or get_current_user_id()
|
| 42 |
+
safe_id = hashlib.md5(uid.encode()).hexdigest()[:12]
|
| 43 |
+
user_dir = USERS_DIR / safe_id
|
| 44 |
+
user_dir.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
(user_dir / "saves").mkdir(exist_ok=True)
|
| 46 |
+
return user_dir
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_user_world_file(user_id: str = None) -> Path:
|
| 50 |
+
return get_user_dir(user_id) / "world.json"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def get_user_saves_dir(user_id: str = None) -> Path:
|
| 54 |
+
return get_user_dir(user_id) / "saves"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_user_active_slot_file(user_id: str = None) -> Path:
|
| 58 |
+
return get_user_dir(user_id) / "active_slot.txt"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def generate_session_id(request=None) -> str:
|
| 62 |
+
"""Generate a unique session ID from Gradio request."""
|
| 63 |
+
if request is not None:
|
| 64 |
+
try:
|
| 65 |
+
ip = getattr(request.client, 'host', '') if hasattr(request, 'client') else ''
|
| 66 |
+
ua = request.headers.get("user-agent", "") if hasattr(request, 'headers') else ""
|
| 67 |
+
if ip or ua:
|
| 68 |
+
return hashlib.md5(f"{ip}:{ua}".encode()).hexdigest()[:16]
|
| 69 |
+
except Exception:
|
| 70 |
+
pass
|
| 71 |
+
return hashlib.md5(os.urandom(16)).hexdigest()[:16]
|
tests/test_player.py
CHANGED
|
@@ -7,7 +7,7 @@ from unittest.mock import patch
|
|
| 7 |
from core.player import (
|
| 8 |
Player, load_player, save_player, get_active_slot, set_active_slot,
|
| 9 |
list_save_slots, switch_slot, delete_slot, rename_player,
|
| 10 |
-
|
| 11 |
)
|
| 12 |
|
| 13 |
|
|
@@ -160,8 +160,8 @@ def test_save_slot_persistence():
|
|
| 160 |
"""Test save/load with slot system."""
|
| 161 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 162 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 163 |
-
patch("core.player.
|
| 164 |
-
patch("core.player.
|
| 165 |
|
| 166 |
# Save to slot 0
|
| 167 |
p = Player()
|
|
@@ -200,8 +200,8 @@ def test_save_slot_delete():
|
|
| 200 |
"""Test deleting a save slot."""
|
| 201 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 202 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 203 |
-
patch("core.player.
|
| 204 |
-
patch("core.player.
|
| 205 |
|
| 206 |
p = Player()
|
| 207 |
p.name = "待删除"
|
|
@@ -219,8 +219,8 @@ def test_rename_player():
|
|
| 219 |
"""Test renaming the current player."""
|
| 220 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 221 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 222 |
-
patch("core.player.
|
| 223 |
-
patch("core.player.
|
| 224 |
|
| 225 |
save_player(Player())
|
| 226 |
rename_player("新名字")
|
|
|
|
| 7 |
from core.player import (
|
| 8 |
Player, load_player, save_player, get_active_slot, set_active_slot,
|
| 9 |
list_save_slots, switch_slot, delete_slot, rename_player,
|
| 10 |
+
_slot_path, _get_slots_dir, _get_active_slot_file,
|
| 11 |
)
|
| 12 |
|
| 13 |
|
|
|
|
| 160 |
"""Test save/load with slot system."""
|
| 161 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 162 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 163 |
+
patch("core.player._get_slots_dir", lambda: Path(tmpdir) / "saves"), \
|
| 164 |
+
patch("core.player._get_active_slot_file", lambda: Path(tmpdir) / "active_slot.txt"):
|
| 165 |
|
| 166 |
# Save to slot 0
|
| 167 |
p = Player()
|
|
|
|
| 200 |
"""Test deleting a save slot."""
|
| 201 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 202 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 203 |
+
patch("core.player._get_slots_dir", lambda: Path(tmpdir) / "saves"), \
|
| 204 |
+
patch("core.player._get_active_slot_file", lambda: Path(tmpdir) / "active_slot.txt"):
|
| 205 |
|
| 206 |
p = Player()
|
| 207 |
p.name = "待删除"
|
|
|
|
| 219 |
"""Test renaming the current player."""
|
| 220 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 221 |
with patch("core.player.PLAYER_FILE", Path(tmpdir) / "player.json"), \
|
| 222 |
+
patch("core.player._get_slots_dir", lambda: Path(tmpdir) / "saves"), \
|
| 223 |
+
patch("core.player._get_active_slot_file", lambda: Path(tmpdir) / "active_slot.txt"):
|
| 224 |
|
| 225 |
save_player(Player())
|
| 226 |
rename_player("新名字")
|
ui/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from ui.explorer_tab import _get_location_choices, _render_location, explore_loc
|
|
| 12 |
from ui.journal_tab import _render_journal, get_quest_choices, complete_quest_action, find_quest_id_by_name
|
| 13 |
from ui.player_tab import _render_player_sheet, switch_save_slot, create_character
|
| 14 |
from core.player import load_player, list_save_slots, get_active_slot
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
# ============================================================
|
|
@@ -43,7 +44,7 @@ def _hero_html():
|
|
| 43 |
{t('hero_subtitle')}
|
| 44 |
</p>
|
| 45 |
<div style="display:flex;justify-content:center;gap:16px;">
|
| 46 |
-
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">
|
| 47 |
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">llama.cpp</span>
|
| 48 |
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">Local-First</span>
|
| 49 |
</div>
|
|
@@ -113,6 +114,9 @@ def create_app():
|
|
| 113 |
initial_slot = get_active_slot()
|
| 114 |
|
| 115 |
with gr.Blocks(title="DreamLand") as app:
|
|
|
|
|
|
|
|
|
|
| 116 |
# Hero
|
| 117 |
gr.HTML(_hero_html())
|
| 118 |
|
|
@@ -218,7 +222,8 @@ def create_app():
|
|
| 218 |
# ===== EVENT WIRING =====
|
| 219 |
|
| 220 |
# Slot selection
|
| 221 |
-
def _on_slot_click(idx):
|
|
|
|
| 222 |
slots = list_save_slots()
|
| 223 |
s = next((s for s in slots if s["slot"] == idx), None)
|
| 224 |
is_new = not s or s["level"] == 0
|
|
@@ -234,12 +239,14 @@ def create_app():
|
|
| 234 |
|
| 235 |
for i, btn in enumerate(slot_btns):
|
| 236 |
btn.click(
|
| 237 |
-
fn=lambda idx=i: _on_slot_click(idx),
|
|
|
|
| 238 |
outputs=[slot_panel, action_btn, selected_slot],
|
| 239 |
)
|
| 240 |
|
| 241 |
# Action button (create or enter)
|
| 242 |
-
def _on_action(slot_idx, name):
|
|
|
|
| 243 |
slots = list_save_slots()
|
| 244 |
s = next((s for s in slots if s["slot"] == slot_idx), None)
|
| 245 |
is_new = not s or s["level"] == 0
|
|
@@ -258,18 +265,19 @@ def create_app():
|
|
| 258 |
|
| 259 |
action_btn.click(
|
| 260 |
fn=_on_action,
|
| 261 |
-
inputs=[selected_slot, name_input],
|
| 262 |
outputs=[slot_panel, action_status, player_display],
|
| 263 |
)
|
| 264 |
|
| 265 |
# Dream submission
|
| 266 |
-
def _process_and_refresh(dream_text, img):
|
|
|
|
| 267 |
results = process_dream(dream_text, img)
|
| 268 |
return (*results, _render_player_sheet())
|
| 269 |
|
| 270 |
submit_btn.click(
|
| 271 |
fn=_process_and_refresh,
|
| 272 |
-
inputs=[dream_input, image_input],
|
| 273 |
outputs=[
|
| 274 |
status_display, dream_text_state,
|
| 275 |
loc_name, loc_desc,
|
|
@@ -288,19 +296,28 @@ def create_app():
|
|
| 288 |
map_refresh.click(fn=_generate_map_html, outputs=[map_display])
|
| 289 |
|
| 290 |
# Explorer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
exp_dropdown.change(fn=_render_location, inputs=[exp_dropdown], outputs=[exp_display])
|
| 292 |
-
explore_btn.click(fn=
|
| 293 |
-
rest_btn.click(fn=
|
| 294 |
|
| 295 |
# Journal
|
| 296 |
-
def _complete_quest(quest_name):
|
|
|
|
| 297 |
if not quest_name:
|
| 298 |
return t("journal_select_quest"), _render_journal()
|
| 299 |
qid = find_quest_id_by_name(quest_name)
|
| 300 |
msg = complete_quest_action(qid)
|
| 301 |
return msg, _render_journal()
|
| 302 |
|
| 303 |
-
complete_btn.click(fn=_complete_quest, inputs=[quest_dropdown], outputs=[journal_result, journal_display])
|
| 304 |
journal_refresh.click(fn=_render_journal, outputs=[journal_display])
|
| 305 |
|
| 306 |
# Player refresh
|
|
|
|
| 12 |
from ui.journal_tab import _render_journal, get_quest_choices, complete_quest_action, find_quest_id_by_name
|
| 13 |
from ui.player_tab import _render_player_sheet, switch_save_slot, create_character
|
| 14 |
from core.player import load_player, list_save_slots, get_active_slot
|
| 15 |
+
from core.session import set_current_user, generate_session_id
|
| 16 |
|
| 17 |
|
| 18 |
# ============================================================
|
|
|
|
| 44 |
{t('hero_subtitle')}
|
| 45 |
</p>
|
| 46 |
<div style="display:flex;justify-content:center;gap:16px;">
|
| 47 |
+
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">MiniCPM4.1-8B</span>
|
| 48 |
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">llama.cpp</span>
|
| 49 |
<span style="padding:4px 14px;border:1px solid #1e1e36;border-radius:20px;font-size:0.72rem;color:#7878a0;">Local-First</span>
|
| 50 |
</div>
|
|
|
|
| 114 |
initial_slot = get_active_slot()
|
| 115 |
|
| 116 |
with gr.Blocks(title="DreamLand") as app:
|
| 117 |
+
# Session state for per-user isolation
|
| 118 |
+
session_id = gr.State(generate_session_id())
|
| 119 |
+
|
| 120 |
# Hero
|
| 121 |
gr.HTML(_hero_html())
|
| 122 |
|
|
|
|
| 222 |
# ===== EVENT WIRING =====
|
| 223 |
|
| 224 |
# Slot selection
|
| 225 |
+
def _on_slot_click(idx, sid):
|
| 226 |
+
set_current_user(sid)
|
| 227 |
slots = list_save_slots()
|
| 228 |
s = next((s for s in slots if s["slot"] == idx), None)
|
| 229 |
is_new = not s or s["level"] == 0
|
|
|
|
| 239 |
|
| 240 |
for i, btn in enumerate(slot_btns):
|
| 241 |
btn.click(
|
| 242 |
+
fn=lambda idx=i, sid=session_id: _on_slot_click(idx, sid),
|
| 243 |
+
inputs=[session_id],
|
| 244 |
outputs=[slot_panel, action_btn, selected_slot],
|
| 245 |
)
|
| 246 |
|
| 247 |
# Action button (create or enter)
|
| 248 |
+
def _on_action(slot_idx, name, sid):
|
| 249 |
+
set_current_user(sid)
|
| 250 |
slots = list_save_slots()
|
| 251 |
s = next((s for s in slots if s["slot"] == slot_idx), None)
|
| 252 |
is_new = not s or s["level"] == 0
|
|
|
|
| 265 |
|
| 266 |
action_btn.click(
|
| 267 |
fn=_on_action,
|
| 268 |
+
inputs=[selected_slot, name_input, session_id],
|
| 269 |
outputs=[slot_panel, action_status, player_display],
|
| 270 |
)
|
| 271 |
|
| 272 |
# Dream submission
|
| 273 |
+
def _process_and_refresh(dream_text, img, sid):
|
| 274 |
+
set_current_user(sid)
|
| 275 |
results = process_dream(dream_text, img)
|
| 276 |
return (*results, _render_player_sheet())
|
| 277 |
|
| 278 |
submit_btn.click(
|
| 279 |
fn=_process_and_refresh,
|
| 280 |
+
inputs=[dream_input, image_input, session_id],
|
| 281 |
outputs=[
|
| 282 |
status_display, dream_text_state,
|
| 283 |
loc_name, loc_desc,
|
|
|
|
| 296 |
map_refresh.click(fn=_generate_map_html, outputs=[map_display])
|
| 297 |
|
| 298 |
# Explorer
|
| 299 |
+
def _explore(loc, sid):
|
| 300 |
+
set_current_user(sid)
|
| 301 |
+
return explore_location(loc)
|
| 302 |
+
|
| 303 |
+
def _rest(loc, sid):
|
| 304 |
+
set_current_user(sid)
|
| 305 |
+
return rest_action(loc)
|
| 306 |
+
|
| 307 |
exp_dropdown.change(fn=_render_location, inputs=[exp_dropdown], outputs=[exp_display])
|
| 308 |
+
explore_btn.click(fn=_explore, inputs=[exp_dropdown, session_id], outputs=[exp_result, exp_display])
|
| 309 |
+
rest_btn.click(fn=_rest, inputs=[exp_dropdown, session_id], outputs=[exp_result, exp_display])
|
| 310 |
|
| 311 |
# Journal
|
| 312 |
+
def _complete_quest(quest_name, sid):
|
| 313 |
+
set_current_user(sid)
|
| 314 |
if not quest_name:
|
| 315 |
return t("journal_select_quest"), _render_journal()
|
| 316 |
qid = find_quest_id_by_name(quest_name)
|
| 317 |
msg = complete_quest_action(qid)
|
| 318 |
return msg, _render_journal()
|
| 319 |
|
| 320 |
+
complete_btn.click(fn=_complete_quest, inputs=[quest_dropdown, session_id], outputs=[journal_result, journal_display])
|
| 321 |
journal_refresh.click(fn=_render_journal, outputs=[journal_display])
|
| 322 |
|
| 323 |
# Player refresh
|