This commit is contained in:
Freddie 2020-06-01 03:43:16 +01:00
parent 510453ce09
commit 0a35d3f544
15 changed files with 498 additions and 87 deletions

126
asphyxia-core.d.ts vendored
View File

@ -1,4 +1,5 @@
/// <reference types="node" />
/// <reference types="lodash" />
declare type KNumberType =
| 's8'
@ -657,6 +658,54 @@ declare namespace $ {
function STR(data: any, path: string, def?: string): string;
}
/** @ignore */
declare interface KITEM<
S extends
| KNumberType
| KBigIntType
| KNumberGroupType
| KBigIntGroupType
| 'str'
| 'bool'
| 'bin'
| 'ip4'
> {
'@attr': {
__type: S;
};
'@content': S extends 'str'
? string
: S extends 'bin'
? Buffer
: S extends KNumberType | 'ip4' | 'bool'
? [number]
: S extends KBigIntType
? [bigint]
: S extends KNumberGroupType
? number[]
: S extends KBigIntGroupType
? bigint[]
: unknown;
}
/** @ignore */
declare interface KARRAY<S extends KNumberType | KBigIntType> {
'@attr': {
__count: number;
__type: S;
};
'@content': S extends KNumberType
? number[]
: S extends KBigIntType
? bigint[]
: unknown;
}
/** @ignore */
declare interface KATTR<M> {
'@attr': M;
}
/**
* K stands for `Konstruct`
*
@ -682,7 +731,7 @@ declare namespace K {
* @param attr Attribute map
* @param inner Inner tag/data
*/
function ATTR(attr: KAttrMap, inner?: any): any;
function ATTR<M extends KAttrMap, T>(attr: M, inner?: T): KATTR<M> & T;
/**
* Example:
@ -699,22 +748,30 @@ declare namespace K {
* @param content data of specified type
* @param attr attribute map in addition to **__type**
*/
function ITEM(type: 'str', content: string, attr?: KAttrMap): any;
function ITEM(type: 'bin', content: Buffer, attr?: KAttrMap): any;
function ITEM(type: 'ip4', content: string, attr?: KAttrMap): any;
function ITEM(type: 'bool', content: boolean, attr?: KAttrMap): any;
function ITEM(type: KNumberType, content: number, attr?: KAttrMap): any;
function ITEM(type: KBigIntType, content: bigint, attr?: KAttrMap): any;
function ITEM(
type: KNumberGroupType,
function ITEM(type: 'str', content: string, attr?: KAttrMap): KITEM<'str'>;
function ITEM(type: 'bin', content: Buffer, attr?: KAttrMap): KITEM<'bin'>;
function ITEM(type: 'ip4', content: string, attr?: KAttrMap): KITEM<'ip4'>;
function ITEM(type: 'bool', content: boolean, attr?: KAttrMap): KITEM<'bool'>;
function ITEM<S extends KNumberType>(
type: S,
content: number,
attr?: KAttrMap
): KITEM<S>;
function ITEM<S extends KBigIntType>(
type: S,
content: bigint,
attr?: KAttrMap
): KITEM<S>;
function ITEM<S extends KNumberGroupType>(
type: S,
content: number[],
attr?: KAttrMap
): any;
function ITEM(
type: KBigIntGroupType,
): KITEM<S>;
function ITEM<S extends KBigIntGroupType>(
type: S,
content: bigint[],
attr?: KAttrMap
): any;
): KITEM<S>;
/**
* Example:
@ -731,9 +788,21 @@ declare namespace K {
* @param content array of data, ____count__ attribute will be automatically set to `content.length`
* @param attr attribute map in addition to **__type** and **__count**
*/
function ARRAY(type: 'u8' | 's8', content: Buffer, attr?: KAttrMap): any;
function ARRAY(type: KNumberType, content: number[], attr?: KAttrMap): any;
function ARRAY(type: KBigIntType, content: bigint[], attr?: KAttrMap): any;
function ARRAY<S extends 'u8' | 's8'>(
type: S,
content: Buffer,
attr?: KAttrMap
): KARRAY<S>;
function ARRAY<S extends KNumberType>(
type: S,
content: number[],
attr?: KAttrMap
): KARRAY<S>;
function ARRAY<S extends KBigIntType>(
type: S,
content: bigint[],
attr?: KAttrMap
): KARRAY<S>;
}
/**
@ -867,6 +936,8 @@ declare namespace U {
/** @ignore */
type Doc<T> = { _id?: string } & T;
/** @ignore */
type ProfileDoc<T> = { _id?: string; __refid?: string } & T;
/** @ignore */
type Query<T> = {
[P in keyof T]?:
| T[P]
@ -978,15 +1049,24 @@ type Update<T> = Partial<T> & {
*
* If you need to make rival/friend feature, we recommend you to get all profile data by passing null to `refid`.
* There will be 16 profiles maximum which is small enough to manage.
*
* All query and doc should not have any fields start with "__" with "__refid" being the only exception.
* However, "__refid" field will still be ignored while other "__" starting fields will cause an error to be thrown.
*/
declare namespace DB {
function FindOne<T>(refid: string | null, query: Query<T>): Promise<Doc<T>>;
function FindOne<T>(
refid: string | null,
query: Query<T>
): Promise<ProfileDoc<T>>;
function FindOne<T>(query: Query<T>): Promise<Doc<T>>;
function Find<T>(refid: string | null, query: Query<T>): Promise<Doc<T>[]>;
function Find<T>(
refid: string | null,
query: Query<T>
): Promise<ProfileDoc<T>[]>;
function Find<T>(query: Query<T>): Promise<Doc<T>[]>;
function Insert<T>(refid: string, doc: T): Promise<Doc<T>>;
function Insert<T>(refid: string, doc: T): Promise<ProfileDoc<T>>;
function Insert<T>(doc: T): Promise<Doc<T>>;
function Remove<T>(refid: string | null, query: Query<T>): Promise<number>;
@ -998,7 +1078,7 @@ declare namespace DB {
update: Update<T>
): Promise<{
updated: number;
docs: Doc<T>[];
docs: ProfileDoc<T>[];
}>;
function Update<T>(
query: Query<T>,
@ -1014,7 +1094,7 @@ declare namespace DB {
update: Update<T>
): Promise<{
updated: number;
docs: Doc<T>[];
docs: ProfileDoc<T>[];
upsert: boolean;
}>;
function Upsert<T>(
@ -1031,7 +1111,5 @@ declare namespace DB {
}
/** @ignore */
declare namespace _ {}
/** @ignore */
// @ts-ignore
declare const _: any;
/// <reference types="lodash" />

View File

@ -24,6 +24,7 @@ export const EVENT5 = [
'STANDARD_UNLOCK_ENABLE',
'PLAYERJUDGEADJ_ENABLE',
'MIXID_INPUT_ENABLE',
'SERIALCODE_JAPAN',
'EVENTDATE_ONIGO',
'EVENTDATE_GOTT',
'GENERATOR_ABLE',
@ -2588,7 +2589,18 @@ export const SDVX_AUTOMATION_SONGS = [
export const EXTENDS5 = [
{
id: 91,
type: 14,
params: [0, 1, 0, 0, 1, SDVX_AUTOMATION_SONGS.join(','), '', '', '', ''],
type: 17,
params: [
0,
1,
0,
0,
1,
SDVX_AUTOMATION_SONGS.join(','),
'0',
'0',
'0',
'0',
],
},
];

View File

@ -1,5 +1,6 @@
import { EVENT4, COURSES4, EXTENDS4 } from '../data/hvn';
import { EVENT5, COURSES5, EXTENDS5 } from '../data/vvw';
export const common: EPR = async (info, data, send) => {
let events = [];
let courses = [];

View File

@ -0,0 +1,126 @@
import { Profile } from '../models/profile';
import { MusicRecord } from '../models/music_record';
import { IDToCode, GetCounter } from '../utils';
import { Mix } from '../models/mix';
export const hiscore: EPR = async (info, data, send) => {
const records = await DB.Find<MusicRecord>(null, { collection: 'music' });
const profiles = _.groupBy(
await DB.Find<Profile>(null, { collection: 'profile' }),
'__refid'
);
send.object({
sc: {
d: _.map(
_.groupBy(records, r => {
return `${r.mid}:${r.type}`;
}),
r => _.maxBy(r, 'score')
).map(r => ({
id: K.ITEM('u32', r.mid),
ty: K.ITEM('u32', r.type),
a_sq: K.ITEM('str', IDToCode(profiles[r.__refid][0].id)),
a_nm: K.ITEM('str', profiles[r.__refid][0].name),
a_sc: K.ITEM('u32', r.score),
l_sq: K.ITEM('str', IDToCode(profiles[r.__refid][0].id)),
l_nm: K.ITEM('str', profiles[r.__refid][0].name),
l_sc: K.ITEM('u32', r.score),
})),
},
});
};
export const rival: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
const rivals = (
await DB.Find<Profile>(null, { collection: 'profile' })
).filter(p => p.__refid != refid);
send.object({
rival: await Promise.all(
rivals.map(async (p, index) => {
return {
no: K.ITEM('s16', index),
seq: K.ITEM('str', IDToCode(p.id)),
name: K.ITEM('str', p.name),
music: (
await DB.Find<MusicRecord>(p.__refid, { collection: 'music' })
).map(r => ({
param: K.ARRAY('u32', [r.mid, r.type, r.score, r.clear, r.grade]),
})),
};
})
),
});
};
export const saveMix: EPR = async (info, data, send) => {
const refid = $(data).str('ref_id');
if (!refid) return send.deny();
const profile = await DB.FindOne<Profile>(refid, { collection: 'profile' });
if (!profile) return send.deny();
const mix = $(data).element('automation');
const id = await GetCounter('mix');
let code = _.padStart(_.random(0, 999999999999).toString(), 12, '0');
while (await DB.FindOne<Mix>(null, { code })) {
code = _.padStart(_.random(0, 999999999999).toString(), 12, '0');
}
const doc = await DB.Insert<Mix>(refid, {
collection: 'mix',
id,
code,
name: mix.str('mix_name'),
creator: profile.name,
param: mix.str('generate_param'),
tag: mix.number('tag_bit'),
jacket: mix.number('jacket_id'),
});
send.object({
automation: {
mix_id: K.ITEM('s32', id),
mix_code: K.ITEM('str', doc.code),
seq: K.ITEM('str', doc.code),
mix_name: K.ITEM('str', doc.name),
player_name: K.ITEM('str', doc.creator),
generate_param: K.ITEM('str', doc.param),
distribution_date: K.ITEM('u32', 19990101),
jacket_id: K.ITEM('s32', doc.jacket),
tag_bit: K.ITEM('s32', doc.tag),
like_flg: K.ITEM('bool', 0),
},
});
};
export const loadMix: EPR = async (info, data, send) => {
const code = $(data).str('mix_code');
const mix = await DB.FindOne<Mix>(null, { collection: 'mix', code });
if (!mix) {
send.object({ result: K.ITEM('s32', 1) });
return;
}
send.object({
automation: {
mix_id: K.ITEM('s32', mix.id),
mix_code: K.ITEM('str', mix.code),
seq: K.ITEM('str', mix.code),
mix_name: K.ITEM('str', mix.name),
player_name: K.ITEM('str', mix.creator),
generate_param: K.ITEM('str', mix.param),
distribution_date: K.ITEM('u32', 19990101),
jacket_id: K.ITEM('s32', mix.jacket),
tag_bit: K.ITEM('s32', mix.tag),
like_flg: K.ITEM('bool', 0),
},
});
};

View File

@ -1,19 +1,35 @@
import { Profile } from '../models/profile';
import { VersionData } from '../models/version_data';
import { Skill } from '../models/skill';
import { SDVX_AUTOMATION_SONGS } from '../data/vvw';
import { CourseRecord } from '../models/course_record';
import { Item } from '../models/item';
import { Param } from '../models/param';
import { MusicRecord } from '../models/music_record';
import { CourseRecord } from '../models/course_record';
import { Profile } from '../models/profile';
import { IDToCode } from '../utils';
import { Mix } from '../models/mix';
function getVersion(info: EamuseInfo) {
if (info.module == 'game_3') return 0;
if (info.method.startsWith('sv4')) return 4;
if (info.method.startsWith('sv5')) return 5;
return 0;
}
export const loadScores: EPR = async (info, data, send) => {
async function getAutomationMixes(params: Param[]) {
const mixids = params
.filter(p => p.id == 3)
.reduce((res, p) => _.union(res, p.param), []);
return await DB.Find<Mix>(null, { collection: 'mix', id: { $in: mixids } });
}
function unlockNavigators(items: Partial<Item>[]) {
for (let i = 0; i < 300; ++i) items.push({ type: 11, id: i, param: 15 });
// 10 genesis card for MITSURU's voice
items.push({ type: 4, id: 599, param: 10 });
return items;
}
export const loadScore: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
@ -45,23 +61,23 @@ export const loadScores: EPR = async (info, data, send) => {
});
};
export const saveScores: EPR = async (info, data, send) => {
export const saveScore: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
const musicID = $(data).number('music_id');
const musicType = $(data).number('music_type');
const mid = $(data).number('music_id');
const type = $(data).number('music_type');
if (musicID == null || musicType == null) return send.deny();
if (_.isNil(mid) || _.isNil(type)) return send.deny();
const record = (await DB.FindOne<MusicRecord>(refid, {
collection: 'music',
mid: musicID,
type: musicType,
mid,
type,
})) || {
collection: 'music',
mid: musicID,
type: musicType,
mid,
type,
score: 0,
clear: 0,
grade: 0,
@ -83,17 +99,44 @@ export const saveScores: EPR = async (info, data, send) => {
await DB.Upsert<MusicRecord>(
refid,
{
collection: 'music',
mid: musicID,
type: musicType,
},
{ collection: 'music', mid, type },
record
);
send.success();
};
export const saveCourse: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
const version = getVersion(info);
if (version == 0) return send.deny();
const sid = $(data).number('ssnid');
const cid = $(data).number('crsid');
if (_.isNil(sid) || _.isNil(cid)) return send.deny();
await DB.Upsert<CourseRecord>(
refid,
{ collection: 'course', sid, cid, version },
{
$max: {
score: $(data).number('sc', 0),
clear: $(data).number('ct', 0),
grade: $(data).number('gr', 0),
rate: $(data).number('ar', 0),
},
$inc: {
count: 1,
},
}
);
send.success();
};
export const save: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
@ -101,6 +144,7 @@ export const save: EPR = async (info, data, send) => {
const version = getVersion(info);
if (version == 0) return send.deny();
// Save Profile
await DB.Update<Profile>(
refid,
{ collection: 'profile' },
@ -133,16 +177,51 @@ export const save: EPR = async (info, data, send) => {
}
);
await DB.Upsert<VersionData>(
// Save Items
const items = $(data).elements('item.info');
for (const i of items) {
const type = i.number('type');
const id = i.number('id');
const param = i.number('param');
if (_.isNil(type) || _.isNil(id) || _.isNil(param)) continue;
await DB.Upsert<Item>(
refid,
{ collection: 'item', type, id },
{ $set: { param } }
);
}
// Save Param
const params = $(data).elements('param.info');
for (const p of params) {
const type = p.number('type');
const id = p.number('id');
const param = p.numbers('param');
if (_.isNil(type) || _.isNil(id) || _.isNil(param)) continue;
await DB.Upsert<Param>(
refid,
{ collection: 'param', type, id },
{ $set: { param } }
);
}
// Save version specific data
await DB.Upsert<Skill>(
refid,
{
collection: 'version',
collection: 'skill',
version,
},
{
$set: {
skillBase: $(data).number('skill_base_id'),
skillLevel: $(data).number('skill_level'),
skillName: $(data).number('skill_name_id'),
base: $(data).number('skill_base_id'),
level: $(data).number('skill_level'),
name: $(data).number('skill_name_id'),
},
}
);
@ -166,22 +245,28 @@ export const load: EPR = async (info, data, send) => {
return;
}
let versionData = await DB.FindOne<VersionData>(refid, {
collection: 'version',
let skill = (await DB.FindOne<Skill>(refid, {
collection: 'skill',
version,
});
})) || { base: 0, name: 0, level: 0 };
const courses = await DB.Find<CourseRecord>(refid, { collection: 'course' });
const items = await DB.Find<Item>(refid, { collection: 'item' });
const params = await DB.Find<Param>(refid, { collection: 'param' });
const mixes = version == 5 ? await getAutomationMixes(params) : [];
send.pugFile('templates/load.pug', {
courses,
items,
items: U.GetConfig('unlock_all_navigators')
? unlockNavigators(items)
: items,
params,
mixes: version == 5 ? SDVX_AUTOMATION_SONGS : [],
skill,
mixes,
automation: version == 5 ? SDVX_AUTOMATION_SONGS : [],
code: IDToCode(profile.id),
...profile,
...versionData,
});
};
@ -190,11 +275,16 @@ export const create: EPR = async (info, data, send) => {
if (!refid) return send.deny();
const name = $(data).str('name', 'GUEST');
let id = _.random(0, 99999999);
while (await DB.FindOne<Profile>(null, { collecttion: 'profile', id })) {
id = _.random(0, 99999999);
}
const profile: Profile = {
pluginVer: 1,
collection: 'profile',
id,
name,
appeal: 0,
akaname: 0,
@ -221,3 +311,48 @@ export const create: EPR = async (info, data, send) => {
await DB.Upsert(refid, { collection: 'profile' }, profile);
send.object({ result: K.ITEM('u8', 0) });
};
export const buy: EPR = async (info, data, send) => {
const refid = $(data).str('refid');
if (!refid) return send.deny();
const growth = {
blocks: $(data).number('earned_gamecoin_block', 0),
packets: $(data).number('earned_gamecoin_packet', 0),
};
const currency = $(data).bool('currency_type') ? 'blocks' : 'packets';
const cost = _.sum($(data).numbers('item.price', []));
const balanceChange = growth[currency] - cost;
const updated = await DB.Update<Profile>(
refid,
{ collection: 'profile', [currency]: { $gte: -balanceChange } },
{ $inc: { [currency]: balanceChange } }
);
if (updated.updated) {
const items = _.zipWith(
$(data).numbers('item.item_type', []),
$(data).numbers('item.item_id', []),
$(data).numbers('item.param', []),
(type, id, param) => ({ type, id, param })
);
for (const item of items) {
await DB.Upsert<Item>(
refid,
{ collection: 'item', type: item.type, id: item.id },
{ $set: { param: item.param } }
);
}
send.object({
gamecoin_packet: K.ITEM('u32', updated.docs[0].packets),
gamecoin_block: K.ITEM('u32', updated.docs[0].blocks),
});
} else {
send.success();
}
};

View File

@ -1,5 +1,14 @@
import { common } from './handlers/common';
import { load, create, loadScores, save, saveScores } from './handlers/profile';
import { hiscore, rival, saveMix, loadMix } from './handlers/features';
import {
load,
create,
loadScore,
save,
saveScore,
saveCourse,
buy,
} from './handlers/profiles';
export function register() {
R.GameCode('KFC');
@ -19,14 +28,29 @@ export function register() {
// Profile
MultiRoute('new', create);
MultiRoute('load', load);
MultiRoute('load_m', loadScores);
MultiRoute('load_r', true);
MultiRoute('load_m', loadScore);
MultiRoute('save', save);
MultiRoute('save_m', saveScores);
MultiRoute('save_m', saveScore);
MultiRoute('save_c', saveCourse);
MultiRoute('frozen', true);
MultiRoute('buy', buy);
// Useless
// Features
MultiRoute('hiscore', hiscore);
MultiRoute('load_r', rival);
MultiRoute('save_ap', saveMix);
MultiRoute('load_ap', loadMix);
// Lazy
MultiRoute('lounge', false);
MultiRoute('shop', true);
MultiRoute('save_e', true);
MultiRoute('play_e', true);
MultiRoute('play_s', true);
MultiRoute('entry_s', true);
MultiRoute('entry_e', true);
MultiRoute('exception', true);
R.Route('eventlog.write', true);
R.Unhandled();
}

View File

@ -0,0 +1,6 @@
export interface Counter {
collection: 'counter';
key: string;
value: number;
}

View File

@ -1,11 +1,13 @@
export interface CourseRecord {
collection: 'course';
version: number;
sid: number;
cid: number;
score: number;
clearType: number;
clear: number;
grade: number;
achieveRate: number;
playCount: number;
rate: number;
count: number;
}

View File

@ -1,5 +1,6 @@
export interface Item {
collection: 'item';
type: number;
id: number;
param: number;

View File

@ -0,0 +1,11 @@
export interface Mix {
collection: 'mix';
id: number;
code: string;
name: string;
creator: string;
param: string;
jacket: number;
tag: number;
}

View File

@ -3,6 +3,7 @@ export interface Profile {
pluginVer: number;
id: number;
name: string;
appeal: number;
akaname: number;

View File

@ -0,0 +1,9 @@
export interface Skill {
collection: 'skill';
version: number;
level: number;
base: number;
name: number;
}

View File

@ -1,10 +0,0 @@
// Version specific data (e.g. skills level)
export interface VersionData {
collection: 'version';
version: number;
skillLevel: number;
skillBase: number;
skillName: number;
}

View File

@ -1,8 +1,8 @@
game
result(__type="u8") 0
name(__type="str") #{name}
code(__type="str") 1337-6666
sdvx_id(__type="str") 1337-6666
code(__type="str") #{code}
sdvx_id(__type="str") #{code}
gamecoin_packet(__type="u32") #{packets}
gamecoin_block(__type="u32") #{blocks}
appeal_id(__type="u16") #{appeal}
@ -26,10 +26,11 @@ game
narrow_down(__type="u8") #{narrowDown}
kac_id(__type="str") #{name}
skill_level(__type="s16") #{skillLevel || 0}
skill_base_id(__type="s16") #{skillBase || 0}
skill_name_id(__type="s16") #{skillName || 0}
skill_level(__type="s16") #{skill.level}
skill_base_id(__type="s16") #{skill.base}
skill_name_id(__type="s16") #{skill.name}
ea_shop
packet_booster(__type="s32") 1
if version != 5
@ -47,10 +48,10 @@ game
ssnid(__type="s16") #{course.sid}
crsid(__type="s16") #{course.cid}
sc(__type="s32") #{course.score}
ct(__type="s16") #{course.clearType}
ct(__type="s16") #{course.clear}
gr(__type="s16") #{course.grade}
ar(__type="s16") #{course.achieveRate}
cnt(__type="s16") #{course.playCount}
ar(__type="s16") #{course.rate}
cnt(__type="s16") #{course.count}
item
each item in items
@ -60,7 +61,7 @@ game
param(__type="u32") #{item.param}
//- Unlock automation songs
each song in mixes
each song in automation
info
type(__type="u8") 15
id(__type="u32") #{song}
@ -69,9 +70,9 @@ game
param
each param in params
info
type(__type="u8") #{param.type}
id(__type="u32") #{param.id}
param(__type="u32" __count=param.param.length) #{param.param.join(" ")}
type(__type="s32") #{param.type}
id(__type="s32") #{param.id}
param(__type="s32" __count=param.param.length) #{param.param.join(" ")}
//- Akaname
each id in [0, 1, 2]
@ -99,7 +100,7 @@ game
mix_name(__type="str") #{mix.name}
player_name(__type="str") #{mix.creator}
generate_param(__type="str") #{mix.param}
distribution_date(__type="u32") 19990101
jacket_id(__type="s32") #{mix.jacketID}
distribution_date(__type="u32") 20200101
jacket_id(__type="s32") #{mix.jacket}
tag_bit(__type="s32") #{mix.tag}
like_flg(__type="u8") 0

14
sdvx@asphyxia/utils.ts Normal file
View File

@ -0,0 +1,14 @@
import { Counter } from './models/counter';
export function IDToCode(id: number) {
const padded = _.padStart(id.toString(), 8);
return `${padded.slice(0, 4)}-${padded.slice(4)}`;
}
export async function GetCounter(key: string) {
return (
await DB.Upsert<Counter>(
{ collection: 'counter', key: 'mix' },
{ $inc: { value: 1 } }
)
).docs[0].value;
}