Created 'Controller Input Display'

This commit is contained in:
sup39 2023-02-06 04:35:42 +09:00
parent d75bdaa3ff
commit 3c1fd9c1a2
14 changed files with 754 additions and 10 deletions

311
Codes.xml
View file

@ -4764,4 +4764,315 @@
4E800021 00000000
</source>
</code>
<code>
<id>controller</id>
<category>metadata</category>
<title lang="en-US">Controller Input Display</title>
<title lang="ja-JP">コントローラ入力表示</title>
<author>sup39(サポミク)</author>
<version>0.1</version>
<date>Feb 05, 2023</date>
<description lang="en-US">
Display controller input.
</description>
<description lang="ja-JP">
コントローラ入力を表示します。
</description>
<source version="GMSJ01">
C20F9CCC 00000047
4E800021 9421FEB0
BE610008 3C003A04
7C17E3A6 3F00817F
3B1804C3 83AD8DF0
7FBDEB79 41820208
838D8DF4 480000B0
38800000 3D80800B
398C88A8 7D8903A6
4E800420 7F2803A6
4E800020 3BE5FFFF
7F2802A6 7CBE0034
3BDEFFEF 4BFFFFD5
E0577000 E077F002
82770004 5FE0F4BA
7C1C042E 7C3D042E
10000C20 100010DC
F01B3000 B35B0000
927B0000 37FFFFFF
4080FFDC 3AF70008
4BFFFFAC 3BE50000
7F2802A6 4BFFFF8D
57E007BC 1017078C
F01BB000 37FFFFFF
57E007BC 60000001
1017078C F01BB000
B35B0000 927B0000
4181FFD8 3AF70004
4BFFFF6C 38610050
7EC4B378 3D808003
398C6B38 4BFFFF51
88180000 98030038
80180001 90030084
90030098 E0185005
102004A0 F0038090
F02380A0 3D808003
398C6CEC 4BFFFF21
3860FFFF 38800001
3D808024 398C2F80
4BFFFF0D 3B400000
3F60CC00 637B8000
3EA08040 A6950D50
3AF80015 386000A0
38A00004 8277FFF4
4BFFFF45 3AD70030
88970003 5E8427FF
41A20014 386000A0
38A00020 4BFFFED9
3AF7FFF8 386000B0
38A00021 4BFFFEC9
7C17B040 4180FFD4
3AD70014 3AB5003C
88970004 5E8427FF
E017A008 C4350004
40820010 E017A000
E057C005 EC0100BA
F017A002 386000A0
38A00004 8278000D
4BFFFED5 3AF70002
386000B0 38A00005
82780011 4BFFFEC1
7C17B040 4180FFB4
3AD70020 3AB5006C
E0172008 E4350040
10400850 10211460
E057A003 1001009C
F0172000 386000A0
38A00020 4BFFFE39
386000B0 38A00009
4BFFFE2D 7C17B040
4180FFC8 BA610008
38210150 00000000
</source>
<source version="GMSJ0A">
C2286124 00000047
4E800021 9421FEB0
BE610008 3C003A04
7C17E3A6 3F00817F
3B1804C3 83ADA018
7FBDEB79 41820208
838DA01C 480000B0
38800000 3D808034
398CD888 7D8903A6
4E800420 7F2803A6
4E800020 3BE5FFFF
7F2802A6 7CBE0034
3BDEFFEF 4BFFFFD5
E0577000 E077F002
82770004 5FE0F4BA
7C1C042E 7C3D042E
10000C20 100010DC
F01B3000 B35B0000
927B0000 37FFFFFF
4080FFDC 3AF70008
4BFFFFAC 3BE50000
7F2802A6 4BFFFF8D
57E007BC 1017078C
F01BB000 37FFFFFF
57E007BC 60000001
1017078C F01BB000
B35B0000 927B0000
4181FFD8 3AF70004
4BFFFF6C 38610050
7EC4B378 3D80802D
398CC7DC 4BFFFF51
88180000 98030038
80180001 90030084
90030098 E0185005
102004A0 F0038090
F02380A0 3D80802D
398CC990 4BFFFF21
3860FFFF 38800001
3D808016 398C3F7C
4BFFFF0D 3B400000
3F60CC00 637B8000
3EA0803F A6955428
3AF80015 386000A0
38A00004 8277FFF4
4BFFFF45 3AD70030
88970003 5E8427FF
41A20014 386000A0
38A00020 4BFFFED9
3AF7FFF8 386000B0
38A00021 4BFFFEC9
7C17B040 4180FFD4
3AD70014 3AB5003C
88970004 5E8427FF
E017A008 C4350004
40820010 E017A000
E057C005 EC0100BA
F017A002 386000A0
38A00004 8278000D
4BFFFED5 3AF70002
386000B0 38A00005
82780011 4BFFFEC1
7C17B040 4180FFB4
3AD70020 3AB5006C
E0172008 E4350040
10400850 10211460
E057A003 1001009C
F0172000 386000A0
38A00020 4BFFFE39
386000B0 38A00009
4BFFFE2D 7C17B040
4180FFC8 BA610008
38210150 00000000
</source>
<source version="GMSP01">
C229E1D8 00000047
4E800021 9421FEB0
BE610008 3C003A04
7C17E3A6 3F00817F
3B1804C3 83ADA090
7FBDEB79 41820208
838DA094 480000B0
38800000 3D808035
398C61A8 7D8903A6
4E800420 7F2803A6
4E800020 3BE5FFFF
7F2802A6 7CBE0034
3BDEFFEF 4BFFFFD5
E0577000 E077F002
82770004 5FE0F4BA
7C1C042E 7C3D042E
10000C20 100010DC
F01B3000 B35B0000
927B0000 37FFFFFF
4080FFDC 3AF70008
4BFFFFAC 3BE50000
7F2802A6 4BFFFF8D
57E007BC 1017078C
F01BB000 37FFFFFF
57E007BC 60000001
1017078C F01BB000
B35B0000 927B0000
4181FFD8 3AF70004
4BFFFF6C 38610050
7EC4B378 3D80802E
398C5174 4BFFFF51
88180000 98030038
80180001 90030084
90030098 E0185005
102004A0 F0038090
F02380A0 3D80802E
398C5328 4BFFFF21
3860FFFF 38800001
3D808018 398C91DC
4BFFFF0D 3B400000
3F60CC00 637B8000
3EA08040 A695BBF4
3AF80015 386000A0
38A00004 8277FFF4
4BFFFF45 3AD70030
88970003 5E8427FF
41A20014 386000A0
38A00020 4BFFFED9
3AF7FFF8 386000B0
38A00021 4BFFFEC9
7C17B040 4180FFD4
3AD70014 3AB5003C
88970004 5E8427FF
E017A008 C4350004
40820010 E017A000
E057C005 EC0100BA
F017A002 386000A0
38A00004 8278000D
4BFFFED5 3AF70002
386000B0 38A00005
82780011 4BFFFEC1
7C17B040 4180FFB4
3AD70020 3AB5006C
E0172008 E4350040
10400850 10211460
E057A003 1001009C
F0172000 386000A0
38A00020 4BFFFE39
386000B0 38A00009
4BFFFE2D 7C17B040
4180FFC8 BA610008
38210150 00000000
</source>
<source version="GMSE01">
C22A62C8 00000047
4E800021 9421FEB0
BE610008 3C003A04
7C17E3A6 3F00817F
3B1804C3 83ADA158
7FBDEB79 41820208
838DA15C 480000B0
38800000 3D808036
398CDF88 7D8903A6
4E800420 7F2803A6
4E800020 3BE5FFFF
7F2802A6 7CBE0034
3BDEFFEF 4BFFFFD5
E0577000 E077F002
82770004 5FE0F4BA
7C1C042E 7C3D042E
10000C20 100010DC
F01B3000 B35B0000
927B0000 37FFFFFF
4080FFDC 3AF70008
4BFFFFAC 3BE50000
7F2802A6 4BFFFF8D
57E007BC 1017078C
F01BB000 37FFFFFF
57E007BC 60000001
1017078C F01BB000
B35B0000 927B0000
4181FFD8 3AF70004
4BFFFF6C 38610050
7EC4B378 3D80802F
398CCFCC 4BFFFF51
88180000 98030038
80180001 90030084
90030098 E0185005
102004A0 F0038090
F02380A0 3D80802F
398CD180 4BFFFF21
3860FFFF 38800001
3D808018 398C2BF8
4BFFFF0D 3B400000
3F60CC00 637B8000
3EA08040 A6954454
3AF80015 386000A0
38A00004 8277FFF4
4BFFFF45 3AD70030
88970003 5E8427FF
41A20014 386000A0
38A00020 4BFFFED9
3AF7FFF8 386000B0
38A00021 4BFFFEC9
7C17B040 4180FFD4
3AD70014 3AB5003C
88970004 5E8427FF
E017A008 C4350004
40820010 E017A000
E057C005 EC0100BA
F017A002 386000A0
38A00004 8278000D
4BFFFED5 3AF70002
386000B0 38A00005
82780011 4BFFFEC1
7C17B040 4180FFB4
3AD70020 3AB5006C
E0172008 E4350040
10400850 10211460
E057A003 1001009C
F0172000 386000A0
38A00020 4BFFFE39
386000B0 38A00009
4BFFFE2D 7C17B040
4180FFC8 BA610008
38210150 00000000
</source>
</code>
</codes>

View file

@ -1,5 +1,7 @@
# Changelog
## Feb 05, 2023
### Created 'Controller Input Display'
Display controller input
### Created 'Attempt Counter'
Display attempt count and success count of current area
### Updated 'Instant Level Select'

View file

@ -3,13 +3,21 @@
<div class="preview-ctn">
<PreviewString v-for="c in previews"
:key="c.key" :config="c" :version="_version" />
<PreviewString v-for="c,i in config.CustomizedDisplay||[]"
:key="'CustomizedDisplay-'+i" :config="c" :version="_version" />
<ControllerPreview v-if="config.controller" :config="config.controller" />
</div>
</div>
</template>
<script>
import {previewIds} from './codes/preview.js';
import ControllerPreview from './codes/controller/preview.vue';
export default {
components: {
ControllerPreview,
},
props: {
config: {type: Object},
},
@ -21,12 +29,10 @@ export default {
previews() {
return previewIds.flatMap(id => {
const config = /**@type{any}*/(this.config)[id];
// special
if (['controller', 'CustomizedDisplay'].includes(id)) return [];
if (config == null) return [];
if (config instanceof Array) {
return config.map((c, i) => ({...c, key: `${id}-${i}`}));
} else {
return {...config, key: id};
}
return {...config, key: id};
});
},
},

View file

@ -4,6 +4,7 @@ import qfst from './qfst/codegen.js';
import CustomizedDisplay from './CustomizedDisplay/codegen.js';
import PatternSelector from './PatternSelector/codegen.js';
import AttemptCounter from './AttemptCounter/codegen.js';
import controller from './controller/codegen.js';
export default {
InstantRestart,
@ -12,6 +13,7 @@ export default {
CustomizedDisplay,
PatternSelector,
AttemptCounter,
controller,
};
/**

View file

@ -0,0 +1,106 @@
import { parseJSON } from '../codegen.js';
import { float2hex, int2hex } from '../utils.js';
import hiddenConfig from './hidden.js';
import { SHIFTS, makeRect, makeNgon, makeTriggerInfo } from './utils.js';
export const lskey = 'config/controller';
export const defaultConfig = {
x: 16,
y: 314,
lw: 20,
height: 120,
bgRGB: 0,
bgA: 0x7f,
};
/** @returns {typeof defaultConfig & typeof hiddenConfig} */
export function getConfig() {
const config =
(typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey))) || {};
return {
...defaultConfig,
...config,
...hiddenConfig,
};
}
/**
* @param {keyof typeof import('../addrs.js').ctxSpOff} version
* @param {string=} baseCode
*/
export default function codegen(version, baseCode) {
if (!baseCode) return '';
const {
x,
y,
lw,
height,
bgRGB,
bgA,
bgLeft,
bgRight,
bgTop,
bgBot,
buttons,
cTF,
cTS,
triggers,
sticks,
} = getConfig();
const logQ = 6;
let code = baseCode;
code += '077F04C3 0000007D';
// basic config
code += [
// lw
int2hex(lw, 1),
// mtx.scale
float2hex((2 ** -logQ * height) / 120),
// mtx.x
int2hex(x, 2),
// mtx.y
int2hex(y - 16, 2),
// .conf.bg.color
int2hex((bgRGB << 24) | bgA, 4),
// .conf.trigger.fill
int2hex(cTF, 4),
// .conf.trigger.stroke
int2hex(cTS, 4),
].join('');
// background
code += makeRect(bgLeft, bgTop, bgRight, bgBot);
// buttons
code += buttons.map((c) => makeNgon(c.x, c.y, c.r, SHIFTS[c.id], c.c)).join('');
// triggers
code += triggers
.flatMap((c) => [
// fill
makeRect(c.x, c.y0, c.x + c.w, c.y1),
// info
makeTriggerInfo(SHIFTS[c.id], c.wa),
// stroke
makeRect(c.x, c.y0, c.x + c.w, c.y1),
])
.join('');
// sticks
code += sticks
.flatMap((c) => [
// fill
makeNgon(-1, -1, c.rF, c.rMove, c.cF),
// stroke
makeNgon(c.x, c.y, c.rS, -1, c.cS),
])
.join('');
// padding
code += '000000';
return code.replace(/\s/g, '');
}

View file

@ -0,0 +1,81 @@
<template>
<div>
<section class="appearance">
<h3>{{ l('h3.appearance') }}</h3>
<div>
<div>
<span>{{l('location')}}(</span><input type="number" min="0" max="600" v-model.number="x"><span>, </span><input type="number" min="16" max="464" v-model.number="y"><span>)</span>
</div>
<div>
<span>{{l('size')}}</span><input type="number" min="0" v-model.number="height">
</div>
<div>
<span>{{ l('bgColor') }}</span
><input type="color" :value="rgbI2S(bgRGB)" @change="bgRGB = rgbS2I($event.target.value)" />
<span>{{ l('alpha') }}</span
><input type="number" min="0" max="255" v-model.number="bgA" /><span
>/255={{ (bgA / 2.55).toFixed(1) }}%</span
>
</div>
</div>
</section>
</div>
</template>
<script>
import { defaultConfig, lskey, getConfig } from './codegen.js';
import hiddenConfig from './hidden.js';
import { makeUpdateConfig, makeGetLabel, rgbI2S } from '../utils';
import labels from '../labels.json';
const updateConfig = makeUpdateConfig(lskey, defaultConfig, null, hiddenConfig);
export default {
props: {
version: { type: String },
},
methods: {
updateConfig,
rgbI2S,
},
data() {
const config = getConfig();
return {...config};
},
computed: {
l() {
return makeGetLabel(labels, this.$lang);
},
},
watch: {
x: updateConfig,
y: updateConfig,
lw: updateConfig,
height: updateConfig,
bgRGB: updateConfig,
bgA: updateConfig,
},
};
</script>
<style scoped>
input[type='number'],
td.right {
text-align: right;
}
input[type='number'] {
width: 3em;
margin: 0 2px;
}
.appearance > div {
padding: 0 0 4px;
}
input[type='number'] {
-moz-appearance: textfield;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>

View file

@ -0,0 +1,66 @@
export const buttons = [
{ x: 138, y: 66, r: 18, id: 'A', c: 0x2ee5b8bf },
{ x: 113, y: 89, r: 9, id: 'B', c: 0xff1a1abf },
{ x: 164, y: 50, r: 8, id: 'X', c: 0xeeeeeebf },
{ x: 119, y: 41, r: 8, id: 'Y', c: 0xeeeeeebf },
{ x: 144, y: 34, r: 6, id: 'Z', c: 0x9494ffbf },
{ x: 91, y: 64, r: 5, id: 'S', c: 0xeeeeeebf },
];
export const sticks = [
{
id: 'M',
x: 32,
y: 52,
rMove: 14,
rS: 19,
cS: 0xeeeeeeef,
rF: 12,
cF: 0xeeeeeeef,
},
{
id: 'C',
x: 64,
y: 92,
rMove: 14,
rS: 19,
cS: 0xffd300ef,
rF: 12,
cF: 0xffd300ef,
},
];
export const triggers = [
{
id: 'L',
x: 12,
y0: 10,
y1: 18,
w: 64,
wa: 56,
},
{
id: 'R',
x: 170,
y0: 10,
y1: 18,
w: -64,
wa: -56,
},
];
export default {
// background
bgLeft: 0,
bgRight: 182,
bgTop: 0,
bgBot: 120,
// trigger fill
cTF: 0xdfdfdfbf,
// trigger stroke
cTS: 0xeeeeeebf,
// input
buttons,
triggers,
sticks,
};

View file

@ -0,0 +1,107 @@
<template>
<svg viewBox="0 16 600 448">
<defs>
<polygon id="8gon" points="1,0 0.7071067811865476,0.7071067811865475 0,1 -0.7071067811865475,0.7071067811865476 -1,0 -0.7071067811865477,-0.7071067811865475 0,-1 0.7071067811865474,-0.7071067811865477 1,0" />
</defs>
<g :transform="transform" :stroke-width="lw">
<rect :x="bg.x" :y="bg.y" :width="bg.w" :height="bg.h" :fill="bg.fill" />
<g v-for="c in buttons" :key="c.id">
<circle :cx="c.x" :cy="c.y" :r="c.r" fill="none" :stroke="c.color" />
</g>
<g v-for="c in sticks" :key="c.id">
<use :stroke-width="lw/c.stroke.scale" :transform="c.stroke.transform" :stroke="c.stroke.color" fill="none" href="#8gon" />
<circle :cx="c.fill.x" :cy="c.fill.y" :r="c.fill.r" :fill="c.fill.color" stroke="none" />
</g>
<g v-for="c in triggers" :key="c.id">
<rect
:x="c.stroke.x" :y="c.stroke.y" :width="c.stroke.w" :height="c.stroke.h"
:stroke="c.stroke.color" fill="none"
/>
<rect
:x="c.fill.x" :y="c.fill.y" :width="c.fill.w" :height="c.fill.h"
:fill="c.fill.color" stroke="none"
/>
</g>
</g>
</svg>
</template>
<script>
import {rgbaI2S, cI2S} from '../utils.js';
export default {
props: {
config: Object,
},
computed: {
transform() {
const {x, y, height} = this.config;
return `translate(${x||0}, ${y||0}) scale(${(height||0)/120})`;
},
lw() {
const {lw} = this.config;
return lw/6;
},
bg() {
const {bgRGB, bgA, bgLeft, bgRight, bgTop, bgBot} = this.config;
return {
x: bgLeft,
y: bgTop,
w: bgRight - bgLeft,
h: bgBot - bgTop,
fill: rgbaI2S(bgRGB, bgA),
};
},
buttons() {
return this.config.buttons.map(c => ({
...c,
color: cI2S(c.c),
}));
},
sticks() {
return this.config.sticks.map(c => ({
id: c.id,
stroke: {
transform: `translate(${c.x||0}, ${c.y||0}) scale(${c.rS||0})`,
scale: c.rS,
color: cI2S(c.cS),
},
fill: {
x: c.x,
y: c.y,
r: c.rF,
color: cI2S(c.cF),
},
}));
},
triggers() {
const {cTF, cTS, triggers} = this.config;
return triggers.map(c => ({
fill: {
color: cI2S(cTF),
x: c.w > 0 ? c.x : c.x+c.wa,
y: c.y0,
w: Math.abs(c.wa),
h: c.y1-c.y0,
},
stroke: {
color: cI2S(cTS),
x: c.w > 0 ? c.x : c.x+c.w,
y: c.y0,
w: Math.abs(c.w),
h: c.y1-c.y0,
},
}));
},
},
}
</script>
<style scoped>
svg {
width: 600px;
height: 448px;
position: absolute;
top: 16px;
}
</style>

View file

@ -0,0 +1,37 @@
import { int2hex } from '../utils.js';
/** @type {Record<string, number>} */
export const SHIFTS = {
Z: 32 - 4,
R: 32 - 5,
L: 32 - 6,
A: 32 - 8,
B: 32 - 9,
X: 32 - 10,
Y: 32 - 11,
S: 32 - 12,
};
/**
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
export const makeRect = (x0, y0, x1, y1) => [x0, y0, x1, y1].map((x) => int2hex(x, 1)).join('');
/**
* @param {number} x
* @param {number} y
* @param {number} r
* @param {number} s
* @param {number} color
*/
export const makeNgon = (x, y, r, s, color) =>
[x, y, r, s].map((x) => int2hex(x, 1)).join('') + int2hex(color, 4);
/**
* @param {number} shift
* @param {number} WA
*/
export const makeTriggerInfo = (shift, WA) => [shift, WA].map((x) => int2hex(x, 1)).join('');

View file

@ -12,6 +12,7 @@
"alpha": "不透明度=",
"bgColor": "背景色:",
"bgOffset": "背景位置:",
"size": "サイズ:",
"left": "左",
"right": "右",
"top": "上",
@ -35,6 +36,7 @@
"alpha": "Alpha=",
"bgColor": "Background color: ",
"bgOffset": "Background offset: ",
"size": "Size: ",
"left": "Left",
"right": "Right",
"top": "Top",

View file

@ -3,8 +3,16 @@ import * as qfst from './qfst/codegen.js';
import * as CustomizedDisplay from './CustomizedDisplay/codegen.js';
import * as PatternSelector from './PatternSelector/codegen.js';
import * as AttemptCounter from './AttemptCounter/codegen.js';
import * as controller from './controller/codegen.js';
export const previewIds = ['CustomizedDisplay', 'AttemptCounter', 'PatternSelector', 'qft', 'qfst'];
export const previewIds = [
'CustomizedDisplay',
'AttemptCounter',
'PatternSelector',
'qft',
'qfst',
'controller',
];
/**
* Get code configs for preview
@ -18,5 +26,6 @@ export const getConfigs = (version) =>
CustomizedDisplay,
PatternSelector,
AttemptCounter,
controller,
}).map(([k, v]) => [k, v.getConfig(version)]),
);

View file

@ -4,6 +4,7 @@ import PatternSelector from './PatternSelector/config.vue';
import qft from './qft/config.vue';
import qfst from './qfst/config.vue';
import AttemptCounter from './AttemptCounter/config.vue';
import controller from './controller/config.vue';
export default {
InstantRestart,
@ -12,4 +13,5 @@ export default {
qft,
qfst,
AttemptCounter,
controller,
};

View file

@ -2,9 +2,10 @@
* @template T extends {Record<string, any>|Record<string, any>[]}
* @param {string} lskey
* @param {T} defaultConfig
* @param {(config: T)=>string} [makeText]
* @param {((config: T)=>string)|null} [makeText]
* @param {any} [hiddenConfig]
*/
export function makeUpdateConfig(lskey, defaultConfig, makeText) {
export function makeUpdateConfig(lskey, defaultConfig, makeText, hiddenConfig = {}) {
const configKeys = Object.keys(defaultConfig);
/** @type {(o: any)=>T} */
const makeConfig =
@ -18,7 +19,8 @@ export function makeUpdateConfig(lskey, defaultConfig, makeText) {
const config = makeConfig(this);
localStorage.setItem(lskey, JSON.stringify(config));
// emit `config` event to parent
this.$emit('config', makeText ? { ...config, text: makeText(config) } : config);
const configEmit = { ...hiddenConfig, ...config };
this.$emit('config', makeText ? { ...configEmit, text: makeText(config) } : configEmit);
};
}
@ -33,6 +35,15 @@ export const int2hex = (x, size) =>
.padStart(size << 1, '0')
.slice(-(size << 1));
/**
* @param {number} x -- number to convert
*/
export function float2hex(x) {
const dv = new DataView(new ArrayBuffer(4));
dv.setFloat32(0, x);
return int2hex(dv.getUint32(0), 4);
}
/** @param {number} rgb */
export const rgbI2S = (rgb) => '#' + rgb.toString(16).padStart(6, '0');
/** @param {string} s */
@ -43,6 +54,8 @@ export const rgbS2I = (s) => parseInt(s.slice(1), 16);
*/
export const rgbaI2S = (rgb, a) =>
'#' + rgb.toString(16).padStart(6, '0') + a.toString(16).padStart(2, '0');
/** @param {number} rgba */
export const cI2S = (rgba) => '#' + (rgba >>> 0).toString(16).padStart(8, '0');
/** @type {(labels: Record<string, any>, locale: string, fallbackLocale?: string) => (key: string) => string} */
export const makeGetLabel =

View file

@ -17,7 +17,7 @@
{
"identifier": "metadata",
"i18nKey": "generatorconfig.categories.metadata",
"exclusive": true
"exclusive": false
},
{
"identifier": "misc",