feat: Title Server Encryption (#216)

i still have a few things i'd like to do but i want to get this merged in and deployed asap
This commit is contained in:
Raymond
2026-04-13 14:54:07 -04:00
committed by GitHub
parent 34cfa279dc
commit 1d3ddfc06c
22 changed files with 348 additions and 122 deletions

3
.gitignore vendored
View File

@@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
test-diff
htmlReport
docs/logs
# Sensitive keys
game_encryption.json

View File

@@ -144,10 +144,9 @@ export const EN_REF_SETUP = {
'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`,
'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`,
'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`,
'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`,
'setup.steps.three': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
'setup.support-info': `If you need assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
'setup.steps.two': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
'setup.support-info': `If you need additional assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
'setup.reveal-keychip': `Reveal keychip`,
'setup.type.automatic': `Automatic Setup`,
'setup.type.manual': `Manual Setup`,
@@ -156,6 +155,15 @@ export const EN_REF_SETUP = {
'setup.automatic.success': `Success, data has been updated`,
'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`,
'setup.automatic.select': `Pick file`,
'setup.troubleshooting.header': 'Setup Troubleshooting (FAQ)',
'setup.troubleshooting.items.one': `» It displays DNS BAD on Network Test<br>
You likely misconfigured your segatools.ini. Ensure it's pointing to the DNS shown above (under Manual Setup).`,
'setup.troubleshooting.items.two': `» It displays タイトル サーバー BAD (Title Server BAD) on Network Test<br>
You should check your segatools.ini to ensure your keychip is set correctly.<br>
Additionally, Card Maker users and players using a game version higher than what's listed may need to disable encryption
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(instructions for Card Maker)</a>`,
'setup.troubleshooting.items.three': `» I can't scan my card<br>
The built-in emulated card reader, by default, is configured to use the Enter / Return key to scan in (hold the key).`,
'setup.keychip': 'Keychip Management',
'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.',
'setup.keychip.add': `Add keychip`,

View File

@@ -5,6 +5,7 @@ import {
EN_REF_LEADERBOARD,
EN_REF_MAI_PHOTO,
EN_REF_SETTINGS,
EN_REF_SETUP,
EN_REF_USER,
EN_REF_USERBOX,
type EN_REF_Welcome
@@ -59,15 +60,15 @@ const zhWelcome: typeof EN_REF_Welcome = {
'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.email-password-missing': '邮箱和密码必须填哦',
'welcome.waiting-turnstile': '正在验证网络环境…',
'welcome.turnstile-error': '验证网络环境出错了请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email}请翻翻收件箱',
'welcome.reset-password-sent': '重置邮件已发送至 ${email}请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件所以我们不会再发送了请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件请翻翻收件箱',
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦请翻翻收件箱!',
'welcome.reset-state-1': '邮件发送次数过多暂时不会再发送新的重置邮件了',
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
'welcome.verifying': '正在验证邮箱…请稍等',
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
'welcome.verification-failed': '验证失败:${message}。请重试',
@@ -90,7 +91,7 @@ const zhGeneral: typeof EN_REF_GENERAL = {
'game.ongeki': "音击",
'game.wacca': "华卡",
"status.error": "发生错误",
"status.error.hint": "出了一些问题请稍后刷新重试或者",
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
"status.error.hint.link": "加我们的 Discord 群问一问",
"status.detail": "详细信息:${detail}",
"action.refresh": "刷新",
@@ -112,21 +113,21 @@ const zhHome: typeof EN_REF_HOME = {
'home.link-card': '绑定游戏卡',
'home.link-cards-description':'绑定游戏数据卡Amusement IC 或 Aime 卡)后才可以访问游戏存档哦',
'home.join-community': '加入群组',
'home.join-community-description': '加入我们的聊天群组与其他玩家聊天、获取帮助',
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
'home.setup': '连接到 AquaDX',
'home.setup-description': '如果您有街机框体或者手台点击这里设置服务器的连接',
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
'home.import': '导入玩家数据',
'home.import-description': '如果你来自其他在线服可以点击这里导入从其他服务器导出的数据',
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
'home.linkcard.cards': "已绑卡片",
'home.linkcard.description': "这些是您绑定到帐户的卡",
'home.linkcard.account-card': "账户卡",
'home.linkcard.registered': "注册于",
'home.linkcard.lastused': "上次使用",
'home.linkcard.enter-info': "请输入以下信息或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡请尝试使用游戏内置的显示卡号功能输入游戏读取到的卡号)",
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
'home.linkcard.enter-sn1': "在您的手机",
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
'home.linkcard.link': "绑定",
'home.linkcard.data-conflict': "卡号冲突",
'home.linkcard.name': "名称",
@@ -137,15 +138,15 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.notfound': "找不到卡",
'home.linkcard.unlink': "取消链接",
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡而非 aime.txt 模拟)刷卡登录游戏与官方服务器不同你需要绑定该卡的 FeliCa SN或与之对应的游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录请忽略本警告继续绑定。",
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台请按照以下说明设置以连接到 AquaDX。",
'home.setup.blockquote': "我们假设您已经拥有所需的文件并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools。如果没有请联系您设备的卖家以获取所需的文件因为出于版权原因我们不会提供这些文件。",
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
'home.setup.get': "开始",
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
'home.setup.test': "在您重新启动游戏后应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
'home.setup.ask': "如果您有任何问题请加入我们的",
'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
'home.setup.ask': "如果您有任何问题,请加入我们的",
'home.setup.support': "以获取支持",
'home.setup.keychip-tips': "这是你的狗号不要与任何人分享",
'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
'home.community.discord': 'Discord',
'home.community.telegram': 'Telegram (中文)',
'home.community.qq': 'QQ (中文)',
@@ -154,6 +155,37 @@ const zhHome: typeof EN_REF_HOME = {
'home.import.data-conflict': '继续导入将覆盖现有数据',
}
const zhSetup: typeof EN_REF_SETUP = {
'setup.welcome': `欢迎!如果您已准备好游戏,请按照以下说明设置与 AquaDX 的连接。`,
'setup.keychip-warning': `您的密钥芯片已与您的账户绑定,请妥善保管。`,
'setup.steps.one': `选择一种网络通信设置方法。某些浏览器可能无法进行自动设置。`,
'setup.steps.two': `请通过 <a href="/cards">卡片</a> 页面,使用访问码或序列号将您的 Aime 卡与 AquaDX 账户绑定。`,
'setup.steps.three': `启动游戏。进入标题画面后,角落处的网络图标应变为绿色,而非灰色。`,
'setup.support-info': `如需进一步协助,欢迎在 <a href="/support">支持频道</a> 中咨询。`,
'setup.reveal-keychip': `显示密钥芯片`,
'setup.type.automatic': `自动设置`,
'setup.type.manual': `手动设置`,
'setup.manual': `请使用以下信息修改您的 segatools.ini 文件。`,
'setup.automatic': `请在下方的 segatools.ini 文件中选择,以自动填写网络信息。`,
'setup.automatic.select': `选择文件`,
'setup.automatic.success': `成功,数据已更新`,
'setup.automatic.failure': `更新信息失败,请确保访问未被阻止,然后重试。`,
'setup.troubleshooting.header': '设置故障排除(常见问题解答)',
'setup.troubleshooting.items.one': `» 网络测试显示 DNS BAD<br>
您可能配置了错误的 segatools.ini。请确保其指向上述手动设置部分显示的 DNS。`,
'setup.troubleshooting.items.two': `» 网络测试显示 タイトル サーバー BAD (Title Server BAD)<br>
请检查您的 segatools.ini 文件,确保密钥芯片设置正确。<br>
此外, Card Maker 用户以及使用高于列表中版本的游戏版本的玩家,可能需要禁用加密
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">Card Maker 操作指南)</a>`,
'setup.troubleshooting.items.three': `» 无法扫描卡片<br>
内置的模拟读卡器默认配置为使用回车键进行扫描(长按该键)。`,
'setup.keychip': '钥匙芯片管理',
'setup.keychip.warning': '对于拥有实体钥匙芯片并试图连接到 AquaDX 的机柜所有者,可在此手动设置钥匙芯片。选择钥匙芯片后,下方设置项将自动填充。',
'setup.keychip.add': `添加钥匙芯片`,
'setup.keychip.delete': `删除`,
'setup.keychip.placeholder': `新钥匙芯片 ID`
}
const zhSettings: typeof EN_REF_SETTINGS = {
'settings.title': '用户设置',
'settings.tabs.profile': '个人资料',
@@ -163,7 +195,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡',
"settings.options.all.preset-warning": "您正在使用预设选项配置部分选项可能显示不正确。",
"settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
"settings.options.all.personal-best": "记录",
"settings.options.all.rival-score": "对手",
"settings.options.all.life": "生命值",
@@ -220,11 +252,11 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」',
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话可以关闭这个(会影响你自己的机器)。',
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
'settings.mai2.name': '玩家名字',
'settings.profile.picture': '头像',
'settings.profile.upload-new': '上传',
'settings.profile.bad-format': '无效的图片格式支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.save': '保存',
'settings.profile.name': '昵称',
'settings.profile.username': '用户名',
@@ -237,7 +269,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unchanged': '未更改',
'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器如果你是在其他人的机器上玩的话请联系机主来改设置',
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
// AI
'settings.siteNotice': "这些设置仅适用于网站。",
// AI
@@ -284,7 +316,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.matching.symbolChat.default': '默认',
'userbox.new.name': 'AquaBox',
'userbox.new.setup': '将中二Lumi 或更高版本)的游戏文件夹拖放到下方区域以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup': '将中二Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup.notice': '选择包含游戏数据的最外层文件夹。',
'userbox.new.setup.processing_file': '正在处理文件',
'userbox.new.setup.finalizing': '正在保存到内部存储',
@@ -298,20 +330,20 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.new.activate_desc': '启用后可显示带有名牌和头像的 UserBox',
'userbox.new.activate_profile': '在用户页面启用 AquaBox',
'userbox.new.activate_profile_desc': '启用后可在个人页面显示带有名牌和头像的 UserBox',
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新并且包含 “A000” 选项包。',
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
};
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai 纪念照片库',
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话请先保存照片再发出去。不要复制图片 URL因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
}
export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.title': '🏳️‍⚧️ AquaTrans™ 数据迁移工具',
'trans.confirm.unbackuped.title': '确认迁移',
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据真的要继续吗?(推荐先备份一下因为迁移的时候会覆盖数据)',
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
'trans.confirm.untested.title': '不太聪明喵',
'trans.confirm.untested.msg': '在两个服务器上都测试完连接之后才能进行数据迁移哦!',
'trans.confirm.done.title': '完成!',
@@ -319,8 +351,8 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.alert.in-progress': '在迁移了在迁移了',
'trans.prompt-html': `
<p>👋 欢迎使用 AquaTrans™ 服务器游玩数据迁移工具!</p>
<p>这个工具可以导出任意服务器的数据并使用连接凭证(卡号、服务器地址和 Keychip ID将数据导入任何其他服务器。</p>
<p>我将模拟游戏客户端从源服务器拉取游戏数据并推送到目标服务器。</p>
<p>这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID将数据导入任何其他服务器。</p>
<p>我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。</p>
<p>填写下面的表格开始迁移吧!</p>
`,
'trans.error.empty': '请填写所有字段。',
@@ -336,11 +368,11 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.btn.test': '测试连接',
'trans.btn.export': '导出数据',
'trans.btn.import': '导入数据',
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了所以不能从这里导出",
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
}
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto,
...zhAquaTrans
...zhAquaTrans, ...zhSetup
}

View File

@@ -201,10 +201,18 @@ ${keychipLines}`.trim(), {
<div class="setup-step">
3. <div>{@html t('setup.steps.three')}</div>
</div>
<div class="setup-step">
4. <div>{@html t('setup.steps.four')}</div>
</div>
<h2>{t('setup.troubleshooting.header')}</h2>
<ul>
<li>
{@html t('setup.troubleshooting.items.one')}
</li>
<li>
{@html t('setup.troubleshooting.items.two')}
</li>
<li>
{@html t('setup.troubleshooting.items.three')}
</li>
</ul>
<p>
{@html t('setup.support-info')}
</p>
@@ -217,6 +225,9 @@ ${keychipLines}`.trim(), {
.code
overflow-x: auto
ul
li
margin: 0.75em 0
.divider
width: 90%
height: 1px

View File

@@ -32,21 +32,22 @@ Multipurpose game server for ALL.Net games.
> [!WARNING]
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Data Import |
|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|
| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ |
| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work | ✅ | ❌ |
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | Encrypt |
|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------|
| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ | ✅ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ | ✅ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
<!-- A majority of them have been left as N/A for initial version as they do not appear to have restrictions -->
Check out these docs for more information.
* [Game specific notes](docs/game_specific_notes.md)
* [Frequently asked questions](docs/frequently_asked_questions.md)
* [Encryption notes](docs/encryption.md)
## Usage

View File

@@ -142,4 +142,4 @@ aqua-net.bot.enabled=true
aqua-net.bot.secret=hunter2
## OpenAI Settings (For content moderation)
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef

35
docs/encryption.md Normal file
View File

@@ -0,0 +1,35 @@
# Encryption Support
AquaDX supports encryption for Chunithm, Maimai and Ongeki.<br>
It is not required to connect and keys are NOT provided by default.
Card Maker has encryption features in the client, but they are not supported due to it's complexity.
## Adding new keys to AquaDX
> [!IMPORTANT]
> Ensure you're using AquaDX from source. Docker will work, as long as you use `docker-compose up --build`
1. Create a new file at `src/main/resources/data/game/<game name>/game_encryption.json`
2. Fill it out using the following schema:
```json
{
"code": "SDHD",
"versions": [215, 216],
"key": "DECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBAD",
"iv": "DECAFBADDECAFBADDECAFBADDECAFBAD",
"salt": "DECAFBADDECAFBAD",
"iterations": 36
},
```
**`key`, `salt`, and `iv` MUST be hex strings. Do NOT insert raw ASCII strings.**<br>
Replace `code` with the correct code for your game (`SDHD` for Chunithm JP, `SDEZ` for Maimai JP, etc.).<br>
Set `versions` to be an array of game versions without the decimal (215 for 2.15, 150 for 1.50, etc.).<br>
You MUST include rom patch versions in this array or it will not encrypt correctly.
- Maimai DX does NOT use iterations, you do not need to include it
- Ongeki always uses 64 iterations (you must include it)
- If using a specific Python script to rip Chunithm keys, focus on the `=>` entries.
- `key` is 32 bytes long. `iv` is 16 bytes long. `salt` is 8 bytes long. (hex strings should be twice as long as they are in bytes)

View File

@@ -185,20 +185,7 @@ class AllNet(
// If keychip authentication is enabled, the game URLs will be set to /gs/{token}/{game}/...
val base = if (session != null) "gs/$session" else "g"
return "http://$addr/$base/" + when (gameId) {
"SDBT" -> "chu2/$ver/$session/"
"SDHD" -> "chu3/$ver/"
"SDGS" -> "chu3/$ver/" // International (c3exp)
"SBZV" -> "diva/"
"SDDT" -> "ongeki/$ver/"
"SDEY" -> "mai/"
"SDGA" -> "mai2/" // International (Exp)
"SDGB" -> "mai2/" // International (China) - TODO: Test it
"SDEZ" -> "mai2/"
"SDFE" -> "wacca" // Note: Wacca must not end with a trailing slash
"SDED" -> "card/"
else -> ""
}
return "http://$addr/$base/$gameId/$ver/"
}
companion object {

View File

@@ -19,7 +19,7 @@ import java.time.format.DateTimeFormatter
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/card")
@RequestMapping("/g/SDED/{version}")
class CardMakerController(
val mapper: BasicMapper,
@param:Value("\${allnet.server.host:}") val ALLNET_HOST: String,

View File

@@ -17,13 +17,18 @@ import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.RestController
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Suppress("unused")
@RestController
@API(value = ["/g/chu3/{version}/ChuniServlet", "/g/chu3/{version}"])
@API(value = [
"/g/SDHD/{version}/ChuniServlet", "/g/SDHD/{version}",
"/g/SDGS/{version}/ChuniServlet", "/g/SDGS/{version}",
])
class ChusanController(
val mapper: StringMapper,
val cmMapper: BasicMapper,
@@ -37,14 +42,19 @@ class ChusanController(
): MeowApi({ api, resp ->
if (resp is String) resp
else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp)
},
@kotlin.ExperimentalStdlibApi
{ endpoint: String ->
db.gameData.chu3GameEncryption.map{
val suffix = when (it.code) { "SDGS" -> "C3Exp" else -> "" }
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.generateSecret(
PBEKeySpec((endpoint + suffix).toCharArray(), it.salt!!.hexToByteArray(), it.iterations, 128)
).encoded.toHexString().substring(0, 32)
}
}) {
val log = LoggerFactory.getLogger(ChusanController::class.java)
val noopEndpoint = setOf("UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi",
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "CreateTokenApi", "RemoveTokenApi", "UpsertClientUploadApi",
"PrinterLoginApi", "PrinterLogoutApi", "Ping", "GameLogoutApi", "RemoveMatchingMemberApi",
"UpsertClientPlayTimeApi", "UpsertClientGameStartApi", "UpsertClientGameEndApi")
init { chusanInit() }
val handlers = initH
@@ -60,28 +70,27 @@ class ChusanController(
data["c3exp"] = true
}
val apiFriendlyName = if (endpoint.lowercase() == endpoint && endpoint.length == 32)
hashes.filter { it.value.contains(api) }.keys.first().str + " (encrypt)" else api
if (api.startsWith("CM") && api !in handlers) api = api.removePrefix("CM")
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":"1","apiName":"$api"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_chusan_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@@ -431,5 +431,23 @@ fun ChusanController.chusanInit() {
// process()
"""{"returnCode":"1"}"""
// NOTE: no-op APIs moved to respect encryption
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
"CreateToken" static { """{returnCode":1, "apiName":"CreateTokenApi"}""" }
"RemoveToken" static { """{returnCode":1, "apiName":"RemoveTokenApi"}""" }
"UpsertClientUpload" static { """{returnCode":1, "apiName":"UpsertClientUploadApi"}""" }
"PrinterLogin" static { """{returnCode":1, "apiName":"PrinterLoginApi"}""" }
"PrinterLogout" static { """{returnCode":1, "apiName":"PrinterLogoutApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
"RemoveMatchingMember" static { """{returnCode":1, "apiName":"RemoveMatchingMemberApi"}""" }
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"UpsertClientPlayTimeApi"}""" }
"UpsertClientGameStart" static { """{returnCode":1, "apiName":"UpsertClientGameStartApi"}""" }
"UpsertClientGameEnd" static { """{returnCode":1, "apiName":"UpsertClientGameEndApi"}""" }
}
}

View File

@@ -35,7 +35,7 @@ val DIVA_INIT = mapOf("db_close" to "0,0", "retry_time" to "FFFF")
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/diva")
@RequestMapping("/g/SBZV/{version}")
class DivaController(
val attendHandler: AttendHandler,
val cardProcedureHandler: CardProcedureHandler,

View File

@@ -31,11 +31,23 @@ typealias PagePost = (MutJDict) -> Unit
data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null)
// A very :3 way of declaring APIs
abstract class MeowApi(val serialize: (String, Any) -> String) {
abstract class MeowApi(
val serialize: (String, Any) -> String,
val endpointHash: ((String) -> List<String>)? = null
) {
val initH = mutableMapOf<String, SpecialHandler>()
val hashes = mutableMapOf<String, MutableList<String>>()
infix operator fun String.invoke(fn: SpecialHandler) {
if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀")
initH["${this}Api"] = fn
if (endpointHash != null) {
// Keep track of hashes for logging; they're not necessary for anything else
hashes["${this}Api"] = mutableListOf()
for (hashedEndpoint in endpointHash("${this}Api")) {
initH[hashedEndpoint] = fn
hashes["${this}Api"]?.add(hashedEndpoint)
}
}
}
infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } }

View File

@@ -5,6 +5,8 @@ import ext.logger
import ext.toJson
import icu.samnyan.aqua.net.components.GeoIP
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
import icu.samnyan.aqua.sega.util.GameDataService
import icu.samnyan.aqua.sega.util.ZLib
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
@@ -14,6 +16,9 @@ import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
@@ -21,21 +26,46 @@ import java.util.*
*/
@Component
class CompressionFilter(
val geoip: GeoIP
val geoip: GeoIP,
val gameData: GameDataService
) : OncePerRequestFilter() {
companion object {
val log = logger()
val b64d = Base64.getMimeDecoder()
val b64e = Base64.getMimeEncoder()
}
var keys = gameData.chu3GameEncryption + gameData.mai2GameEncryption + gameData.ogkGameEncryption
fun getEncryptionKeys(path: String): GameEncryptionKey? {
val endpoint = path.split("/").last()
if (endpoint.lowercase() != endpoint || endpoint.length != 32) return null
val game = path.split("/")[2]
val version = path.split("/")[3].filterNot { it == '.' }.toInt()
return keys.find { it.versions.contains(version) && it.code == game }
}
@OptIn(ExperimentalStdlibApi::class)
override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
val isDeflate = req.getHeader("content-encoding") == "deflate"
val isDfi = req.getHeader("pragma") == "DFI"
val keys = getEncryptionKeys(req.servletPath)
// Decode input
val reqSrc = try {
req.inputStream.readAllBytes().let {
if (keys != null) {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
IvParameterSpec(keys.iv!!.hexToByteArray())
)
cipher.doFinal(it)
} else it
}.let {
if (isDeflate) ZLib.decompress(it)
else if (isDfi) ZLib.decompress(b64d.decode(it))
else it
@@ -50,7 +80,19 @@ class CompressionFilter(
val respW = ContentCachingResponseWrapper(resp)
val result = try {
chain.doFilter(CompressRequestWrapper(req, reqSrc), respW)
ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it }
ZLib.compress(respW.contentAsByteArray)
.let { if (isDfi) b64e.encode(it) else it }
.let {
if (keys != null) {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
IvParameterSpec(keys.iv!!.hexToByteArray())
)
cipher.doFinal(it)
} else it
}
} finally {
if (respW.status != 200) {
val details = mapOf(

View File

@@ -0,0 +1,14 @@
package icu.samnyan.aqua.sega.general.model
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime
class GameEncryptionKey {
var code: String? = null
var versions: List<Int> = listOf()
var key: String? = null
var salt: String? = null
var iv: String? = null
var iterations = 0
}

View File

@@ -424,8 +424,20 @@ fun Maimai2ServletController.initApis() {
)
) }
"UpsertUserPlaceCircleRegist" static { mapOf(
"returnCode" to 0,
"apiName" to "UpsertUserPlaceCircleRegistApi"
) }
// NOTE: no-op APIs moved to respect encryption
"UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") }
"GetUserScoreRanking" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" }
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientBookkeepingApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientTestmodeApi"}""" }
"UpsertClientUpload" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientUploadApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.Ping"}""" }
"RemoveToken" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.RemoveTokenApi"}""" }
"CMLogin" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLoginApi"}""" }
"CMLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLogoutApi"}""" }
"CMUpsertBuyCard" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMUpsertBuyCardApi"}""" }
"UserLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UserLogoutApi"}""" }
"GetGameMapAreaCondition" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetGameMapAreaConditionApi"}""" }
"UpsertUserChargelog" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertUserChargelogApi"}""" }
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientPlayTimeApi"}""" }
}

View File

@@ -11,12 +11,14 @@ import icu.samnyan.aqua.sega.maimai2.handler.*
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import kotlinx.io.bytestring.hexToByteString
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest
import java.time.format.DateTimeFormatter
import kotlin.reflect.full.declaredMemberProperties
@@ -25,7 +27,11 @@ import kotlin.reflect.full.declaredMemberProperties
*/
@Suppress("unused")
@RestController
@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"])
@RequestMapping(path = [
"/g/SDGA/{version}/Maimai2Servlet/", "/g/SDGA/{version}",
"/g/SDEZ/{version}/Maimai2Servlet/", "/g/SDEZ/{version}",
//"/g/SDGB/{version}/Maimai2Servlet/", "/g/SDGB/{version}",
])
class Maimai2ServletController(
val upsertUserAll: UpsertUserAllHandler,
val getUserItem: GetUserItemHandler,
@@ -40,7 +46,18 @@ class Maimai2ServletController(
val getGameRanking: GetGameRankingHandler,
val db: Mai2Repos,
val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
): MeowApi(
serialize = { _, resp -> if (resp is String) resp else resp.toJson() },
@kotlin.ExperimentalStdlibApi
{ endpoint: String ->
db.gameData.mai2GameEncryption.map{
val suffix = when (it.code) { "SDGA" -> "MaimaiExp" "SDGB" -> "MaimaiChn" else -> "" }
MessageDigest.getInstance("MD5")
.digest((endpoint + suffix).toByteArray() + it.salt!!.hexToByteArray())
.toHexString()
}
}
) {
companion object {
private val log = logger()
private val empty = listOf<Any>()
@@ -55,11 +72,6 @@ class Maimai2ServletController(
"CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
"GetUserFavoriteItemApi")
val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
"CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi",
"UpsertUserChargelogApi","UpsertClientPlayTimeApi")
val members = this::class.declaredMemberProperties
val handlers: Map<String, SpecialHandler> = initH + endpointList.associateWith { api ->
val name = api.replace("Api", "").lowercase()
@@ -72,27 +84,25 @@ class Maimai2ServletController(
@API("/{api}")
fun handle(@PathVariable api: String, @RequestBody data: Map<String, Any>, req: HttpServletRequest): Any {
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable {
val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@@ -147,7 +147,7 @@ class Mai2Repos(
val userKaleidx: MAi2UserKaleidxRepo,
val userIntimate: MAi2UserIntimateRepo,
val userRegions: Mai2UserRegionsRepo,
gameData: GameDataService
val gameData: GameDataService
) {
val gameCharge = StaticRepo(gameData.mai2Charges) { it.orderId }
val gameEvent = StaticRepo(gameData.mai2Events) { it.id }

View File

@@ -70,4 +70,16 @@ fun OngekiController.ongekiInit() {
"GetClientTestmode" {
empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"])
}
// NOTE: no-op APIs moved to respect encryption
"ExtendLockTime" static { """{returnCode":1, "apiName":"ExtendLockTimeApi"}""" }
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
"RegisterPromotionCard" static { """{returnCode":1, "apiName":"RegisterPromotionCardApi"}""" }
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
"UpsertUserGplog" static { """{returnCode":1, "apiName":"UpsertUserGplogApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
}

View File

@@ -8,14 +8,18 @@ import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.Maimai2ServletController
import icu.samnyan.aqua.sega.util.BasicMapper
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.RestController
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
@Suppress("unused")
@RestController
@API("/g/ongeki/{version}", "/g/ongeki")
@API("/g/SDDT/{version}", "/g/SDDT")
class OngekiController(
val mapper: BasicMapper,
val db: OngekiUserRepos,
@@ -23,14 +27,23 @@ class OngekiController(
val us: AquaUserServices,
val pop: GameMusicPopularity,
val cardService: CardService,
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
): MeowApi(
{ _, resp -> if (resp is String) resp else mapper.write(resp) },
@OptIn(ExperimentalStdlibApi::class)
{ endpoint: String ->
gdb.gameData.ogkGameEncryption.map{
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.generateSecret(PBEKeySpec(
endpoint.toCharArray(),
it.salt!!.hexToByteArray(),
it.iterations, 128
)).encoded.toHexString().substring(0, 32)
}
}
) {
val log = logger()
val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi",
"UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi",
"UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping")
init { ongekiInit() }
val handlers = initH
@@ -40,26 +53,25 @@ class OngekiController(
version?.let { data["version"] = it }
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.util
import com.fasterxml.jackson.core.type.TypeReference
import ext.logger
import ext.toJson
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
import icu.samnyan.aqua.sega.chusan.model.GameCharge as Chu3GameCharge
import icu.samnyan.aqua.sega.chusan.model.GameEvent as Chu3GameEvent
import icu.samnyan.aqua.sega.chusan.model.GameGacha as Chu3GameGacha
@@ -45,7 +46,8 @@ class GameDataService() {
return mapper.read(resStream.bufferedReader().use { it.readText() }, object : TypeReference<List<T>>() {})
}
log.warn("Game data file $f or resource $resPath not found, using empty list")
if (file != "game_encryption.json")
log.warn("Game data file $f or resource $resPath not found, using empty list")
return emptyList()
}
}
@@ -54,6 +56,7 @@ class GameDataService() {
lateinit var mai2Events: List<Mai2GameEvent>
lateinit var mai2Charges: List<Mai2GameCharge>
lateinit var mai2SellingCards: List<Mai2GameSellingCard>
lateinit var mai2GameEncryption: List<GameEncryptionKey>
// chusan
lateinit var chu3GameLinkedVerses: List<Chu3GameLinkedVerse>
@@ -63,6 +66,7 @@ class GameDataService() {
lateinit var chu3GameGachas: List<Chu3GameGacha>
lateinit var chu3GameLoginBonusPresets: List<Chu3GameLoginBonusPreset>
lateinit var chu3GameLoginBonuses: List<Chu3GameLoginBonus>
lateinit var chu3GameEncryption: List<GameEncryptionKey>
// ongeki
lateinit var ogkGameCards: List<OgkGameCard>
@@ -75,6 +79,7 @@ class GameDataService() {
lateinit var ogkGameSkills: List<OgkGameSkill>
lateinit var ogkGameGachaCards: List<OgkGameGachaCard>
lateinit var ogkGameGachas: List<OgkGameGacha>
lateinit var ogkGameEncryption: List<GameEncryptionKey>
init {
load()
@@ -84,6 +89,7 @@ class GameDataService() {
mai2Events = load("maimai2", "game_event.json")
mai2Charges = load("maimai2", "game_charge.json")
mai2SellingCards = load("maimai2", "game_selling_card.json")
mai2GameEncryption = load(game = "maimai2", file = "game_encryption.json")
chu3GameLinkedVerses = load("chusan", "game_linked_verse.json")
chu3GameCharges = load("chusan", "game_charge.json")
@@ -92,6 +98,7 @@ class GameDataService() {
chu3GameGachas = load("chusan", "game_gacha.json")
chu3GameLoginBonusPresets = load("chusan", "game_login_bonus_preset.json")
chu3GameLoginBonuses = load("chusan", "game_login_bonus.json")
chu3GameEncryption = load(game = "chusan", file = "game_encryption.json")
ogkGameCards = load("ongeki", "game_card.json")
ogkGameCharas = load("ongeki", "game_chara.json")
@@ -103,6 +110,7 @@ class GameDataService() {
ogkGameSkills = load("ongeki", "game_skill.json")
ogkGameGachaCards = load("ongeki", "game_gacha_card.json")
ogkGameGachas = load("ongeki", "game_gacha.json")
ogkGameEncryption = load(game = "ongeki", file = "game_encryption.json")
}
}

View File

@@ -25,7 +25,7 @@ import kotlin.math.max
import kotlin.math.min
@RestController
@API("/g/wacca/")
@API("/g/SDFE/{version}")
class WaccaServer {
// These are lateinit autowired instead of constructor injection because the tests depend on creating
// an instance of this class with no arguments.