diff --git a/launcher/DNF90启动器.spec b/launcher/DNF90启动器.spec index 00cd46f..ad40fe3 100644 --- a/launcher/DNF90启动器.spec +++ b/launcher/DNF90启动器.spec @@ -1,7 +1,8 @@ # -*- mode: python ; coding: utf-8 -*- from PyInstaller.utils.hooks import collect_all -datas = [('index.html', '.'), ('style.css', '.'), ('shell.css', '.'), ('app.js', '.'), ('shell.js', '.'), ('sponsor.png', '.'), ('db.py', '.'), ('native.py', '.'), ('launcher.ico', '.')] +datas = [('index.html', '.'), ('style.css', '.'), ('shell.css', '.'), ('app.js', '.'), ('shell.js', '.'), ('sponsor.png', '.'), ('db.py', '.'), ('native.py', '.'), ('launcher.ico', '.'), + ('qr/alipay.png', 'qr'), ('qr/wechat.png', 'qr'), ('qr/gzh.jpg', 'qr')] binaries = [] hiddenimports = ['clr_loader', 'pythonnet'] tmp_ret = collect_all('webview') diff --git a/launcher/index.html b/launcher/index.html index 9c74bce..79d226e 100644 --- a/launcher/index.html +++ b/launcher/index.html @@ -30,6 +30,7 @@ +
@@ -127,7 +128,55 @@
-

游戏更新

从自托管仓库(Lingbao/90CNServer)的发布记录自动获取。

+
+

游戏更新

优先读取仓库(Lingbao/90CNServer)的发布记录,并用本地「更新日志.txt」补齐仓库还没发布的版本;断网时只显示本地记录。

+
+ + +
+
正在获取更新日志…
+ +
+
+ +
+
+
+

地下城与勇士 · 九零轮回单机

+

汉服电脑宅 · 灵宝小竹模拟器

+

由灵宝仙尊联合淡竹茹开发,由汉服电脑宅团队开发维护

+

本模拟器资源禁止倒卖,请到官方微信公众号或社群下载

+
+ +
+
+
赞助我们
+
+
+ 支付宝收款码 +
支付宝
+
+
+ 微信收款码 +
微信
+
+
+
金额随意,感谢支持;扫码后备注游戏名,方便记录。
+
+
+
团队公众号
+
+
+ 团队公众号二维码 +
扫码关注
+
+
+
更新公告 · 开服时间 · 玩法说明
+
+
+ +

点击任意二维码可放大查看;把同名图片放进启动器目录下的 qr 文件夹即可替换。

+
@@ -165,6 +214,8 @@
+ + diff --git a/launcher/qr/alipay.png b/launcher/qr/alipay.png new file mode 100644 index 0000000..9da38f7 Binary files /dev/null and b/launcher/qr/alipay.png differ diff --git a/launcher/qr/gzh.jpg b/launcher/qr/gzh.jpg new file mode 100644 index 0000000..47a7bf8 Binary files /dev/null and b/launcher/qr/gzh.jpg differ diff --git a/launcher/qr/wechat.png b/launcher/qr/wechat.png new file mode 100644 index 0000000..ba4b11b Binary files /dev/null and b/launcher/qr/wechat.png differ diff --git a/launcher/server.py b/launcher/server.py index 9b36da3..82dbec0 100644 --- a/launcher/server.py +++ b/launcher/server.py @@ -16,6 +16,7 @@ import ctypes import json import os import queue +import re import shutil import socket import subprocess @@ -145,6 +146,16 @@ SPONSOR_IMG = os.path.join(BASE_DIR, "sponsor.png") if not os.path.exists(SPONSOR_IMG): SPONSOR_IMG = os.path.join(RES_DIR, "sponsor.png") +# 关于我们页的三张二维码:优先 EXE 同目录 qr\(可自行替换),其次打包内资源 +QR_FILES = { + "alipay.png": "image/png", + "wechat.png": "image/png", + "gzh.jpg": "image/jpeg", +} + +# 断网时更新日志的兜底来源(EXE 同目录 / 服务端根目录的 更新日志.txt) +LOCAL_CHANGELOG_NAME = "更新日志.txt" + # 数据库与实例配置候选路径(基于运行目录解析出的 SERVER_ROOT) def _instance_candidates(): out = [] @@ -373,6 +384,93 @@ def _release_summary(rel): } +def _local_changelog(): + """断网时的兜底:解析本地 更新日志.txt,按日期切成若干条(最新在前)。 + + 文件长这样(日期行之间是正文): + 2026.11.05 更新: + 1.xxx + 2.xxx + """ + path = "" + for d in (BASE_DIR, SERVER_ROOT): + if not d: + continue + p = os.path.join(d, LOCAL_CHANGELOG_NAME) + if os.path.exists(p): + path = p + break + if not path: + return [] + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + lines = f.read().splitlines() + except Exception: + return [] + + items = [] + cur = None + for ln in lines: + s = ln.strip() + m = re.match(r"^(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})", s) + if m: + if cur: + items.append(cur) + date = "%s.%s.%s" % (m.group(1), m.group(2).zfill(2), m.group(3).zfill(2)) + cur = { + "tag": date, + "name": date + " 更新", + "body": "", + "published_at": "", + "prerelease": False, + "html_url": UPDATE_REPO_WEB, + "assets": [], + } + continue + if cur is not None and s: + cur["body"] = (cur["body"] + "\n" + s).strip() + if cur: + items.append(cur) + items.reverse() # 文件里是旧→新,接口要新→旧 + return items + + +def _changelog_date_key(rel): + """从 tag / name / 发布时间里抽 20261105 这样的排序键;取不到返回空串。""" + s = " ".join([rel.get("tag") or "", rel.get("name") or "", rel.get("published_at") or ""]) + m = re.search(r"(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})", s) + if not m: + return "" + return "%s%02d%02d" % (m.group(1), int(m.group(2)), int(m.group(3))) + + +def _merge_changelog(remote, local): + """仓库发布记录 + 本地更新日志合并:按日期去重(仓库优先,内容更全),新的在前。 + + 仓库没跟上发版时,本地 更新日志.txt 里的新条目会自动出现在「当前更新」。 + """ + out, seen = [], set() + for rel in remote or []: + k = _changelog_date_key(rel) + if k: + if k in seen: + continue + seen.add(k) + out.append(rel) + for rel in local or []: + k = _changelog_date_key(rel) + if k: + if k in seen: + continue + seen.add(k) + out.append(rel) + + dated = [r for r in out if _changelog_date_key(r)] + undated = [r for r in out if not _changelog_date_key(r)] + dated.sort(key=_changelog_date_key, reverse=True) + return dated + undated + + def flow_start_game(account_username=None): """一键开始游戏;account_username 为空时使用默认账号。""" log("== 开始游戏 ==") @@ -590,6 +688,8 @@ class Handler(BaseHTTPRequestHandler): self._send_file(SPONSOR_IMG, "image/png") else: self._json({"error": "赞助二维码尚未提供"}, 404) + elif path.startswith("/qr/"): + self._send_qr(path[len("/qr/"):]) # 启动器接口 elif path == "/api/status": self._api_status() @@ -792,19 +892,39 @@ class Handler(BaseHTTPRequestHandler): }) def _api_update_changelog(self, qs): - """获取所有发布的更新日志。""" + """获取所有发布的更新日志;连不上仓库时退回本地 更新日志.txt。""" force = qs.get("force", ["0"])[0] == "1" releases, err = _fetch_releases(force=force) - if err: - self._json({"ok": False, "error": err, "releases": []}) + remote = [] if err else [_release_summary(r) for r in (releases or [])] + local = _local_changelog() + merged = _merge_changelog(remote, local) + if not merged: + self._json({"ok": False, "error": err or "没有任何更新记录", "releases": []}) return self._json({ "ok": True, "current": CURRENT_VERSION, - "releases": [_release_summary(r) for r in (releases or [])], + "releases": merged, + # local = 完全没连上仓库;merged = 仓库记录 + 本地补齐 + "source": "remote" if remote else "local", + "local_count": len(local), + "warning": err, "web_url": UPDATE_REPO_WEB, }) + def _send_qr(self, name): + """关于我们页的二维码图片:EXE 同目录 qr\\ 优先,其次打包内资源。""" + ctype = QR_FILES.get(name) + if not ctype: + self._json({"error": "not found"}, 404) + return + for d in (os.path.join(BASE_DIR, "qr"), os.path.join(RES_DIR, "qr")): + p = os.path.join(d, name) + if os.path.exists(p): + self._send_file(p, ctype) + return + self._json({"error": "二维码未配置"}, 404) + def _api_logs(self): logs = [] while True: diff --git a/launcher/shell.css b/launcher/shell.css index 62a5ebc..4b0974e 100644 --- a/launcher/shell.css +++ b/launcher/shell.css @@ -393,3 +393,54 @@ body.gm-fullscreen .win-resize{display:none} /* 简易 GM 控制台运行时,完整 GM 工具入口置灰不可点 */ .shell-nav-item.nav-disabled{opacity:.38;cursor:not-allowed;filter:grayscale(1)} .shell-nav-item.nav-disabled:hover{background:transparent;color:inherit} + +/* ================= 游戏更新:当前 / 历史 两个子页 ================= */ +.upd-body{display:flex;flex-direction:column;gap:12px;margin-top:14px} +.upd-count{color:#8d91a8;font-size:12px;margin-left:2px} +.upd-source{margin-bottom:10px;padding:8px 12px;font-size:12px;color:#ffc96b; + background:rgba(255,201,77,.08);border:1px solid rgba(255,201,77,.28);border-radius:8px} +.changelog-item{border:1px solid rgba(255,255,255,.09);background:rgba(20,20,36,.62); + border-radius:10px;padding:16px 18px} +.changelog-item.featured{border-color:rgba(124,92,255,.5); + background:linear-gradient(160deg,rgba(124,92,255,.16),rgba(20,20,36,.7)); + box-shadow:0 6px 22px rgba(124,92,255,.15)} +.changelog-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px} +.changelog-tag{font-family:Consolas,monospace;font-size:12px;color:#c9beff;background:rgba(124,92,255,.16); + padding:2px 8px;border-radius:5px;border:1px solid rgba(124,92,255,.32)} +.changelog-name{font-size:14px;font-weight:600;color:#e2e4f2} +.changelog-time{font-size:11px;color:#6d7188;margin-left:auto} +.changelog-body{font-size:13px;color:#b8bcd0;line-height:1.9;word-break:break-word} +.changelog-assets{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} +.changelog-asset{font-size:11px;color:#9fe6c4;text-decoration:none;border:1px solid rgba(255,255,255,.12); + padding:3px 10px;border-radius:6px;background:rgba(255,255,255,.04)} +.changelog-asset:hover{background:rgba(255,255,255,.1)} +.changelog-link{display:inline-block;margin-top:12px;font-size:11px;color:#8d91a8;text-decoration:none} +.changelog-link:hover{color:#c9beff} + +/* ================= 关于我们 ================= */ +.about-pad{max-width:880px} +.about-hero{padding:18px 22px;border:1px solid rgba(255,255,255,.09);border-radius:12px; + background:linear-gradient(160deg,rgba(124,92,255,.18),rgba(20,20,36,.6))} +.about-hero h2{margin:0 0 10px;font-size:21px;color:#f2f3f8;letter-spacing:.5px} +.about-line{margin:3px 0;color:#b8bcd0;font-size:13px;line-height:1.9} +.about-panel{display:flex;margin-top:14px;border:1px solid rgba(255,255,255,.09);border-radius:12px; + background:rgba(20,20,36,.6);overflow:hidden} +.about-col{padding:16px 20px} +.about-col-l{flex:1 1 auto} +.about-col-r{width:224px;flex:none;border-left:1px solid rgba(255,255,255,.09);background:rgba(0,0,0,.2)} +.about-col-hd{color:var(--gold);font-size:14px;letter-spacing:1px;margin-bottom:12px} +.about-col-bd{display:flex;gap:16px} +.about-qr{margin:0;cursor:zoom-in} +.about-qr img{display:block;width:136px;height:136px;background:#fff;border-radius:8px;padding:4px; + transition:transform .15s,box-shadow .15s} +.about-qr:hover img{transform:translateY(-2px);box-shadow:0 6px 18px rgba(124,92,255,.35)} +.about-qr figcaption{margin-top:7px;text-align:center;font-size:12px;color:#9a9eb5} +.about-col-note{margin-top:12px;font-size:11px;color:#6d7188;line-height:1.8} +.about-tip{margin-top:12px;font-size:11px;color:#5d6178} + +/* 二维码放大 */ +.qr-lightbox{position:fixed;inset:0;z-index:300;background:rgba(5,5,12,.85); + display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;cursor:zoom-out} +.qr-lightbox img{width:min(340px,56vh);height:auto;background:#fff;padding:10px;border-radius:12px; + box-shadow:0 12px 40px rgba(0,0,0,.55)} +.qr-lightbox-tip{color:#b8bcd0;font-size:12px} diff --git a/launcher/shell.js b/launcher/shell.js index 664c000..bfc9cb8 100644 --- a/launcher/shell.js +++ b/launcher/shell.js @@ -1,13 +1,13 @@ /* 90轮回启动器 · 单窗口页面与工具整合 */ (function () { const $ = (id) => document.getElementById(id); - const pages = ['start','changelog','gm','loadout','gamesettings']; + const pages = ['start','changelog','gm','loadout','gamesettings','about']; let shellGmInit = false; let loConfig = null, loPage = 0, loScheme = -1; let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null; let hotkeyCapture = false; - const crumb = {start:'开始 / 本地服务端',changelog:'游戏更新',gm:'GM工具',loadout:'工具 / 一键换装',gamesettings:'工具 / 游戏设置'}; + const crumb = {start:'开始 / 本地服务端',changelog:'游戏更新',gm:'GM工具',loadout:'工具 / 一键换装',gamesettings:'工具 / 游戏设置',about:'关于我们'}; function shellGo(page) { if (!pages.includes(page)) page = 'start'; @@ -258,43 +258,60 @@ } } - // 更新日志页面:拉取所有 Release 的 changelog - async function loadChangelog() { - const box = $('changelogBody'); - if (!box) return; - box.innerHTML = '
正在获取更新日志…
'; + // 更新日志:当前更新 = 最新一条;历史更新 = 其余。优先 Gitea,断网退回本地 更新日志.txt + function updDate(rel) { + return rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : ''; + } + + function updCard(rel, featured) { + const badge = rel.prerelease ? '预发布' : ''; + const body = (rel.body || '').replace(/\r?\n/g, '
'); + const assets = (rel.assets || []).map(a => + `⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)` + ).join(''); + const when = updDate(rel); + return `
+
+ ${escapeHtml(rel.tag || rel.name || '')} + ${escapeHtml(rel.name || '')} + ${badge} + ${escapeHtml(when)} +
+
${body || '(这次没有填写更新说明)'}
+ ${assets ? '
' + assets + '
' : ''} + ${featured ? `在浏览器中查看 ↗` : ''} +
`; + } + + async function loadChangelog(force) { + const latestBox = $('updLatest'), historyBox = $('updHistory'); + if (!latestBox || !historyBox) return; + latestBox.innerHTML = '
正在获取更新日志…
'; + historyBox.innerHTML = ''; try { - const r = await api('/api/update/changelog'); + const r = await api('/api/update/changelog' + (force ? '?force=1' : '')); if (!r.ok) { - box.innerHTML = '
获取失败:' + escapeHtml(r.error || '未知错误') + '
'; + latestBox.innerHTML = '
获取失败:' + escapeHtml(r.error || '未知错误') + '
'; + if ($('updHistoryCount')) $('updHistoryCount').textContent = ''; return; } const rels = r.releases || []; if (!rels.length) { - box.innerHTML = '
暂无发布记录。
在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。
'; + latestBox.innerHTML = '
暂无更新记录。
在仓库发布 Release,或把「更新日志.txt」放在启动器目录,这里就会显示。
'; + if ($('updHistoryCount')) $('updHistoryCount').textContent = ''; return; } - box.innerHTML = rels.map(rel => { - const badge = rel.prerelease ? '预发布' : ''; - const body = (rel.body || '').replace(/\r?\n/g, '
'); - const assets = (rel.assets || []).map(a => - `⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)` - ).join(''); - const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : ''; - return `
-
- ${escapeHtml(rel.tag || rel.name || '')} - ${escapeHtml(rel.name || '')} - ${badge} - ${escapeHtml(when)} -
-
${body}
- ${assets ? '
' + assets + '
' : ''} - 在浏览器中查看 ↗ -
`; - }).join(''); + const source = r.source === 'local' + ? '
未连接更新仓库,以下内容读取自本地「更新日志.txt」。
' + : ''; + latestBox.innerHTML = source + updCard(rels[0], true); + const rest = rels.slice(1); + historyBox.innerHTML = rest.length + ? rest.map(rel => updCard(rel, false)).join('') + : '
还没有更早的更新记录。
'; + if ($('updHistoryCount')) $('updHistoryCount').textContent = rest.length ? '(' + rest.length + ')' : ''; } catch (e) { - box.innerHTML = '
获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
'; + latestBox.innerHTML = '
获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
'; } } @@ -745,6 +762,41 @@ }); }); + // ---------------- 游戏更新:当前 / 历史 两个子页 ---------------- + (function bindUpdTabs() { + const tabs = document.querySelectorAll('#updTabs .st-tab'); + tabs.forEach(btn => { + btn.addEventListener('click', () => { + const which = btn.dataset.upd; + tabs.forEach(b => b.classList.toggle('active', b === btn)); + $('updLatestPane').style.display = which === 'latest' ? '' : 'none'; + $('updHistoryPane').style.display = which === 'history' ? '' : 'none'; + }); + }); + const refresh = $('updRefresh'); + if (refresh) refresh.addEventListener('click', () => loadChangelog(true)); + })(); + + // ---------------- 关于我们:二维码放大 ---------------- + (function bindQrLightbox() { + const box = $('qrLightbox'), img = $('qrLightboxImg'), tip = $('qrLightboxTip'); + if (!box || !img) return; + document.querySelectorAll('#page-about .about-qr').forEach(fig => { + fig.addEventListener('click', () => { + const src = fig.querySelector('img'); + if (!src) return; + img.src = src.getAttribute('src'); + img.alt = src.getAttribute('alt') || ''; + tip.textContent = (fig.dataset.qr || '') + ' · 点击任意处关闭'; + box.classList.remove('hidden'); + }); + }); + box.addEventListener('click', () => box.classList.add('hidden')); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') box.classList.add('hidden'); + }); + })(); + // 兼容旧 GM 页面里的返回 window.shellGo = shellGo; loadStart();