- 游戏更新
从自托管仓库(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();