Skip to content

Commit bc57f16

Browse files
committed
add ability to output polytone creative tab modifiers
1 parent 94c375f commit bc57f16

File tree

8 files changed

+303
-6
lines changed

8 files changed

+303
-6
lines changed

@types/generated/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
declare module '@pssbletrngle/data-modifier/generated' {
22
export type RegistryId = string
33

4+
export type CreativeModeTabId = string
5+
46
// eslint-disable-next-line @typescript-eslint/no-unused-vars
57
export type InferIds<T extends RegistryId> = string
68

src/cli/codegen/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export function generateStubTypes(outputDirectory: string) {
7474
type StubId = ${stubIdType}
7575
7676
export type RegistryId = StubId
77+
78+
export type CreativeModeTabId = StubId
7779
7880
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7981
export type InferIds<T extends RegistryId> = StubId

src/emit/blacklist.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ type RegistryIdInput<T extends RegistryId> = InferIds<T> | RegExp
2424

2525
export default class BlacklistEmitter implements BlacklistRules, ClearableEmitter {
2626
private hidden: NormalizedId[] = []
27-
private hideModes: HideMode[]
27+
private readonly hideModes: HideMode[]
2828

2929
constructor(
3030
private readonly logger: Logger,
3131
private readonly tags: TagRegistryHolder,
3232
private readonly lookup: () => RegistryLookup,
33-
private options: BlacklistOptions
33+
options: BlacklistOptions
3434
) {
3535
this.hideModes = arrayOrSelf(options.hideFrom)
3636
}

src/emit/custom.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { Acceptor } from '@pssbletrngle/pack-resolver'
2-
import { toJson } from '../textHelper.js'
32
import { createId, IdInput } from '../common/id.js'
43
import Registry from '../common/registry.js'
4+
import { toJson } from '../textHelper.js'
55
import { PathProvider } from './index.js'
66

77
export default class CustomEmitter<TEntry> {
8-
constructor(private readonly pathProvider: PathProvider) {}
8+
constructor(
9+
private readonly pathProvider: PathProvider,
10+
private readonly encoder: (value: TEntry) => string = toJson
11+
) {}
912

10-
private customEntries = new Registry<TEntry>()
13+
private readonly customEntries = new Registry<TEntry>()
1114

1215
clear() {
1316
this.customEntries.clear()
@@ -17,10 +20,23 @@ export default class CustomEmitter<TEntry> {
1720
this.customEntries.set(createId(id), value)
1821
}
1922

23+
merge(id: IdInput, entry: TEntry, merger: (a: TEntry, b: TEntry) => TEntry) {
24+
this.modify(id, existing => {
25+
if (existing) return merger(existing, entry)
26+
return entry
27+
})
28+
}
29+
30+
modify(id: IdInput, factory: (existing?: TEntry) => TEntry) {
31+
const existing = this.customEntries.get(id)
32+
if (existing) this.add(id, factory(existing))
33+
else this.add(id, factory())
34+
}
35+
2036
async emit(acceptor: Acceptor) {
2137
this.customEntries.forEach((entry, id) => {
2238
const path = this.pathProvider(id)
23-
acceptor(path, toJson(entry))
39+
acceptor(path, this.encoder(entry))
2440
})
2541
}
2642

src/emit/polytoneTabs.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { CreativeModeTabId, ItemId } from '@pssbletrngle/data-modifier/generated'
2+
import { Acceptor, arrayOrSelf } from '@pssbletrngle/pack-resolver'
3+
import { difference, uniq } from 'lodash-es'
4+
import { createId, encodeId, IdInput, NormalizedId } from '../common/id.js'
5+
import RegistryLookup from '../loader/registry/index.js'
6+
import CustomEmitter from './custom.js'
7+
import { ClearableEmitter } from './index.js'
8+
9+
type AddOptions =
10+
| {
11+
after: IdInput<ItemId>
12+
}
13+
| {
14+
before: IdInput<ItemId>
15+
}
16+
17+
export interface PolytoneTabs {
18+
remove(tab: IdInput<CreativeModeTabId> | IdInput<CreativeModeTabId>[], items: IdInput<ItemId>[]): void
19+
add(
20+
tab: IdInput<CreativeModeTabId> | IdInput<CreativeModeTabId>[],
21+
items: IdInput<ItemId>[],
22+
options?: AddOptions
23+
): void
24+
create(id: IdInput): void
25+
}
26+
27+
interface ItemsMatchPredicate {
28+
type: 'items_match'
29+
items: NormalizedId<ItemId>[]
30+
}
31+
32+
type PolytonePredicate = ItemsMatchPredicate
33+
34+
interface AdditionEntry {
35+
items: ItemId[]
36+
before?: boolean
37+
inverse?: boolean
38+
predicate?: PolytonePredicate
39+
}
40+
41+
export interface PolytoneTabModifier {
42+
targets: NormalizedId<CreativeModeTabId>[]
43+
removals?: PolytonePredicate[]
44+
additions?: AdditionEntry[]
45+
}
46+
47+
function mergeModifiers(a: PolytoneTabModifier, b: PolytoneTabModifier): PolytoneTabModifier {
48+
const diff = difference(a.targets, b.targets)
49+
if (diff.length) throw new Error('trying to merge modifiers with different targets')
50+
return {
51+
targets: uniq([...a.targets, ...b.targets]),
52+
additions: [...(a.additions ?? []), ...(b.additions ?? [])],
53+
removals: [...(a.removals ?? []), ...(b.removals ?? [])],
54+
}
55+
}
56+
57+
function translateOptions(options?: AddOptions): Partial<AdditionEntry> {
58+
if (!options) return {}
59+
const before = 'before' in options
60+
const item = before ? options.before : options.after
61+
return {
62+
before,
63+
predicate: {
64+
items: [encodeId(item)],
65+
type: 'items_match',
66+
},
67+
}
68+
}
69+
70+
export default class PolytoneTabsEmitter implements PolytoneTabs, ClearableEmitter {
71+
private readonly entries = new CustomEmitter<PolytoneTabModifier>(
72+
id => `assets/${id.namespace}/polytone/creative_tab_modifiers/${id.path}.json`
73+
)
74+
75+
private readonly tabs = new CustomEmitter<string[]>(
76+
id => `assets/${id.namespace}/polytone/creative_tabs.csv`,
77+
it => it.join(',')
78+
)
79+
80+
constructor(private readonly lookup: () => RegistryLookup) {}
81+
82+
clear(): void {
83+
this.entries.clear()
84+
this.tabs.clear()
85+
}
86+
87+
async emit(acceptor: Acceptor) {
88+
await Promise.all([this.entries.emit(acceptor), this.tabs.emit(acceptor)])
89+
}
90+
91+
remove(tab: IdInput<CreativeModeTabId> | IdInput<CreativeModeTabId>[], items: IdInput<ItemId>[]) {
92+
this.forEach(tab, {
93+
removals: [{ type: 'items_match', items: items.map(encodeId) }],
94+
})
95+
}
96+
97+
add(tab: IdInput<CreativeModeTabId> | IdInput<CreativeModeTabId>[], items: IdInput<ItemId>[], options?: AddOptions) {
98+
this.forEach(tab, {
99+
additions: [{ items: items.map(encodeId), ...translateOptions(options) }],
100+
})
101+
}
102+
103+
create(id: IdInput): void {
104+
const lookup = this.lookup()
105+
lookup.addCustom('minecraft:creative_mode_tab', id)
106+
const { namespace, path } = createId(id)
107+
this.tabs.merge({ namespace, path: 'tab' }, [path], (a, b) => uniq([...a, ...b]))
108+
}
109+
110+
private forEach(
111+
tab: IdInput<CreativeModeTabId> | IdInput<CreativeModeTabId>[],
112+
modifier: Omit<PolytoneTabModifier, 'targets'>
113+
) {
114+
arrayOrSelf(tab).forEach(target => {
115+
const entry: PolytoneTabModifier = {
116+
targets: [encodeId(target)],
117+
...modifier,
118+
}
119+
this.entries.merge(target, entry, mergeModifiers)
120+
})
121+
}
122+
}

src/loader/pack.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import LootTableEmitter, { LootRules } from '../emit/data/loot.js'
1313
import RecipeEmitter, { RecipeRules } from '../emit/data/recipe.js'
1414
import TagEmitter, { TagEmitterOptions, TagRules } from '../emit/data/tags.js'
1515
import { ClearableEmitter } from '../emit/index.js'
16+
import PolytoneTabsEmitter, { PolytoneTabs } from '../emit/polytoneTabs.js'
1617
import { Logger } from '../logger.js'
1718
import Loader, { AcceptorWithLoader } from './index.js'
1819
import LangLoader from './lang.js'
@@ -64,6 +65,7 @@ export default class PackLoader implements Loader, ClearableEmitter {
6465
)
6566
)
6667
readonly lang: LangRules = this.registerEmitter(new LangEmitter(this.langLoader))
68+
readonly tabs: PolytoneTabs = this.registerEmitter(new PolytoneTabsEmitter(() => this.activeRegistryLookup))
6769
readonly blacklist: BlacklistRules = this.registerEmitter(
6870
new BlacklistEmitter(this.logger, this.tagLoader, () => this.activeRegistryLookup, this.options)
6971
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`create new tabs > emits and mergers csv > tab csv 1 for something 1`] = `"test_tab,another_tab"`;
4+
5+
exports[`create new tabs > emits and mergers csv > tab csv for minecraft 1`] = `"more_blocks"`;
6+
7+
exports[`create removal entries > resolves filter correctly > removal modifier 1 1`] = `
8+
"{
9+
"targets": ["example:food"],
10+
"removals": [
11+
{ "type": "items_match", "items": ["minecraft:diamond", "forge:the_logo"] }
12+
]
13+
}
14+
"
15+
`;
16+
17+
exports[`create removal entries > resolves filter correctly > removal modifier 2 1`] = `
18+
"{
19+
"targets": ["something:test_tab"],
20+
"removals": [
21+
{ "type": "items_match", "items": ["minecraft:diamond", "forge:the_logo"] }
22+
]
23+
}
24+
"
25+
`;
26+
27+
exports[`creates addition entries > adds after predicate > addition modifier with after predicate 1`] = `
28+
"{
29+
"targets": ["example:tab"],
30+
"additions": [
31+
{
32+
"items": ["minecraft:oak_log"],
33+
"before": false,
34+
"predicate": { "items": ["minecraft:stone_axe"], "type": "items_match" }
35+
}
36+
]
37+
}
38+
"
39+
`;
40+
41+
exports[`creates addition entries > adds before predicate > addition modifier with after predicate 1`] = `
42+
"{
43+
"targets": ["example:tab"],
44+
"additions": [
45+
{
46+
"items": ["minecraft:oak_planks"],
47+
"before": true,
48+
"predicate": { "items": ["minecraft:stone_hoe"], "type": "items_match" }
49+
}
50+
]
51+
}
52+
"
53+
`;
54+
55+
exports[`creates addition entries > emits per tab key > addition modifier 1 1`] = `
56+
"{
57+
"targets": ["example:food"],
58+
"additions": [{ "items": ["minecraft:diamond", "forge:the_logo"] }]
59+
}
60+
"
61+
`;
62+
63+
exports[`creates addition entries > emits per tab key > addition modifier 2 1`] = `
64+
"{
65+
"targets": ["something:test_tab"],
66+
"additions": [{ "items": ["minecraft:diamond", "forge:the_logo"] }]
67+
}
68+
"
69+
`;

test/polytoneTabs.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, it } from 'vitest'
2+
import createTestAcceptor from './mock/TestAcceptor.js'
3+
import setupLoader from './shared/loaderSetup.js'
4+
5+
const { loader } = setupLoader({ load: false, hideFrom: ['polytone'] })
6+
7+
describe('creates addition entries', () => {
8+
it('emits per tab key', async () => {
9+
const acceptor = createTestAcceptor()
10+
11+
loader.tabs.add(
12+
['something:test_tab', { namespace: 'example', path: 'food' }],
13+
['minecraft:diamond', { namespace: 'forge', path: 'the_logo' }]
14+
)
15+
16+
await loader.emit(acceptor)
17+
18+
expect(acceptor.at('assets/something/polytone/creative_tab_modifiers/test_tab.json')).toMatchSnapshot(
19+
'addition modifier 2'
20+
)
21+
expect(acceptor.at('assets/example/polytone/creative_tab_modifiers/food.json')).toMatchSnapshot(
22+
'addition modifier 1'
23+
)
24+
})
25+
26+
it('adds after predicate', async () => {
27+
const acceptor = createTestAcceptor()
28+
29+
loader.tabs.add('example:tab', ['minecraft:oak_log'], { after: 'minecraft:stone_axe' })
30+
31+
await loader.emit(acceptor)
32+
33+
expect(acceptor.at('assets/example/polytone/creative_tab_modifiers/tab.json')).toMatchSnapshot(
34+
'addition modifier with after predicate'
35+
)
36+
})
37+
38+
it('adds before predicate', async () => {
39+
const acceptor = createTestAcceptor()
40+
41+
loader.tabs.add('example:tab', ['minecraft:oak_planks'], { before: 'minecraft:stone_hoe' })
42+
43+
await loader.emit(acceptor)
44+
45+
expect(acceptor.at('assets/example/polytone/creative_tab_modifiers/tab.json')).toMatchSnapshot(
46+
'addition modifier with after predicate'
47+
)
48+
})
49+
})
50+
51+
describe('create removal entries', () => {
52+
it('resolves filter correctly', async () => {
53+
const acceptor = createTestAcceptor()
54+
55+
loader.tabs.remove(
56+
['something:test_tab', { namespace: 'example', path: 'food' }],
57+
['minecraft:diamond', { namespace: 'forge', path: 'the_logo' }]
58+
)
59+
60+
await loader.emit(acceptor)
61+
62+
expect(acceptor.at('assets/something/polytone/creative_tab_modifiers/test_tab.json')).toMatchSnapshot(
63+
'removal modifier 2'
64+
)
65+
expect(acceptor.at('assets/example/polytone/creative_tab_modifiers/food.json')).toMatchSnapshot(
66+
'removal modifier 1'
67+
)
68+
})
69+
})
70+
71+
describe('create new tabs', () => {
72+
it('emits and mergers csv', async () => {
73+
const acceptor = createTestAcceptor()
74+
75+
loader.tabs.create('something:test_tab')
76+
loader.tabs.create('something:another_tab')
77+
loader.tabs.create('minecraft:more_blocks')
78+
79+
await loader.emit(acceptor)
80+
81+
expect(acceptor.at('assets/something/polytone/creative_tabs.csv')).toMatchSnapshot('tab csv 1 for something')
82+
expect(acceptor.at('assets/minecraft/polytone/creative_tabs.csv')).toMatchSnapshot('tab csv for minecraft')
83+
})
84+
})

0 commit comments

Comments
 (0)