Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [main]

jobs:
publish:
build:
runs-on: ubuntu-latest

permissions:
Expand All @@ -17,13 +17,14 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org/

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build
run: bun run prod

# `bun test` (= bun 内蔵ランナー) ではなく package.json の test script を通す。
# テストは vitest で書かれており、 `vi.mock` を使うものは内蔵ランナーでは動かない。
- name: Run tests
run: bun test
run: bun run test
9 changes: 7 additions & 2 deletions src/systems/datapackCompiler/1.20.4-tsb/global.mcb
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,16 @@ dir global {

data remove storage <%temp_storage%> args
data remove storage <%temp_storage%> uuids
data modify storage <%temp_storage%> uuids set from entity @s data.uuids
# UUID 一覧は entity NBT ではなく data_manager が読み込んだ storage 側にある
# (entity の保存 NBT に `data` compound は存在せず、 読もうとすると NbtPath が
# not-found 例外を投げて score が 0 に落ちる = loop に一度も入らない)。
data modify storage <%temp_storage%> uuids set from storage <%temp_storage%> entry.data.uuids
execute store result score #aj.length <%OBJECTIVES.I()%> run data get storage <%temp_storage%> uuids

execute if score #aj.length <%OBJECTIVES.I()%> matches 1.. run block loop_over_uuids {
data modify storage <%temp_storage%> args.current_uuid set from storage <%temp_storage%> uuids[-1].uuid
# 要素は compound ではなく UUID 文字列そのもの (gu の out をそのまま append している)。
# key 名は呼び先 `entity_stack_by_uuid` の `#ARGS: {uuid: string}` に合わせる。
data modify storage <%temp_storage%> args.uuid set from storage <%temp_storage%> uuids[-1]
data remove storage <%temp_storage%> uuids[-1]
function ./entity_stack_by_uuid with storage <%temp_storage%> args

Expand Down
57 changes: 33 additions & 24 deletions src/systems/datapackCompiler/1.20.4-tsb/main.mcb
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,30 @@ function on_load {
}
%%>

# Phase C 案 V : variant 適用テーブルを on_load で同期書き込み (サイズ < 1KB)。
# `aj.<bp>:meta d.variants.<variant>.<bone_id>` = `{cmd?: int, cfg?: compound}`、 影響しない
# bone は path 不在 = no-op。 cleanup の `d` 配下 remove で消えるので残骸残りなし。
# 加えて root On-Apply Function を持つ variant には `on_apply:1b` を並べる
# (= `variants/_apply` の dispatch ガード、 詳細は variants/_apply 側のコメント)。
# TSB Optimized Export : animation_hash 判定で reload 時のキュー再構築をスキップ。
# 同一値の set value は throw + success 0 になる仕様
# (`DataCommands.java::ERROR_MERGE_UNCHANGED`) を利用、 hash 変化時のみ init_queue。
# 部分展開中 reload は hash 一致でスキップされるが、 残った queue は global tick の
# load_dispatch_step が引き続き処理するため最終的に完全展開される。
# datapack 入れ替え時の旧 storage 残骸は許容 (主に同じ cell の上書きで除去、
# 一部 unreferenced cell は残るが運用上問題なし)。
# 詳細 : docs/tsb-known-issues/reload-skip-on-hash-match.md
execute store success score #h <%OBJECTIVES.I()%> run data modify storage <%project_storage.replace(':', '.')%>:state d.animation_hash set value "<%animation_hash%>"
execute if score #h <%OBJECTIVES.I()%> matches 1 run function <%blueprint_id%>/load/init_queue
}

# Phase C 案 V : variant 適用テーブルを on_load で同期書き込み (サイズ < 1KB)。
# `aj.<bp>:meta d.variants.<variant>.<bone_id>` = `{cmd?: int, cfg?: compound}`、 影響しない
# bone は path 不在 = no-op。 TSB 経路は on_load で cleanup を呼ばないため、 残骸は
# 下の `set value` による毎回の上書きで潰す (= gate が false の経路だけ remove が要る)。
# 加えて root On-Apply Function を持つ variant には `on_apply:1b` を並べる
# (= `variants/_apply` の dispatch ガード、 詳細は variants/_apply 側のコメント)。
# animation の有無とは独立に必要なので `needs_variant_functions` で判定する
# (= animation 0 個の blueprint でも variants/_apply の dispatch 条件を成立させる)。
IF (tsb_optimized_export && needs_variant_functions) {
<%%
if (Object.keys(rig.variants).length > 0) {
const filteredBones = Object.values(rig.nodes).filter(n => BONE_TYPES.includes(n.type))
// variant が 1 個だけの blueprint では variants dir 自体を生成しないので、
// on_apply フラグも書かない (= 読む側が存在しない)。
const emitOnApplyFlag = Object.keys(rig.variants).length > 1
const variantParts = []
for (const variant of Object.values(rig.variants)) {
const bonePartsList = []
Expand Down Expand Up @@ -119,26 +132,22 @@ function on_load {
bonePartsList.push(`"${i}":{${parts.join(',')}}`)
})
// on_apply フラグは bone key と同じ compound に並べる (bone key は数値文字列なので衝突しない)。
if (emitOnApplyFlag && variant.on_apply_function) bonePartsList.push('on_apply:1b')
if (variant.on_apply_function) bonePartsList.push('on_apply:1b')
if (bonePartsList.length === 0) continue
variantParts.push(`${variant.name}:{${bonePartsList.join(',')}}`)
}
if (variantParts.length > 0) {
emit(`data modify storage ${project_storage.replace(':', '.')}:meta d.variants set value {${variantParts.join(',')}}`)
}
// 空でも `set value {}` で必ず上書きする (= 前回 export で書いた `on_apply:1b` 等が
// storage に残り、 フラグ不要になった後も dispatch が成立し続けるのを防ぐ)。
emit(`data modify storage ${project_storage.replace(':', '.')}:meta d.variants set value {${variantParts.join(',')}}`)
}
%%>

# TSB Optimized Export : animation_hash 判定で reload 時のキュー再構築をスキップ。
# 同一値の set value は throw + success 0 になる仕様
# (`DataCommands.java::ERROR_MERGE_UNCHANGED`) を利用、 hash 変化時のみ init_queue。
# 部分展開中 reload は hash 一致でスキップされるが、 残った queue は global tick の
# load_dispatch_step が引き続き処理するため最終的に完全展開される。
# datapack 入れ替え時の旧 storage 残骸は許容 (主に同じ cell の上書きで除去、
# 一部 unreferenced cell は残るが運用上問題なし)。
# 詳細 : docs/tsb-known-issues/reload-skip-on-hash-match.md
execute store success score #h <%OBJECTIVES.I()%> run data modify storage <%project_storage.replace(':', '.')%>:state d.animation_hash set value "<%animation_hash%>"
execute if score #h <%OBJECTIVES.I()%> matches 1 run function <%blueprint_id%>/load/init_queue
} ELSE IF (tsb_optimized_export) {
# variant 関連を一切出力しない構成では上の `set value` が出ないため、 旧 export の
# `d.variants` が storage に残り続ける。 TSB 経路は on_load で cleanup を呼ばない
# 方針なので、 この経路だけは明示的に remove する。
# 不在時の `data remove` は ERROR_MERGE_UNCHANGED を投げる (= 初回 load で毎回踏む) ため、
# 存在確認を挟む。
execute if data storage <%project_storage.replace(':', '.')%>:meta d.variants run data remove storage <%project_storage.replace(':', '.')%>:meta d.variants
}
}

Expand Down Expand Up @@ -1383,7 +1392,7 @@ dir remove {
}
}

IF (Object.keys(rig.variants).length > 1) {
IF (needs_variant_functions) {
dir variants {
IF (tsb_optimized_export) {
# Phase C 案 V : variant 適用関数を共通 1 ファイル + wrapper N 個 + dispatch 1 個に集約。
Expand Down
13 changes: 13 additions & 0 deletions src/systems/datapackCompiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,18 @@ const dataPackCompiler: DataPackCompiler = async ({
.map(() => '..')
.join('/')

// variant 関連の関数と metadata を出力する必要があるか。
// rig.variants には default が常に含まれるので length > 1 は「カスタム variant あり」の意味。
// default のみでも root On-Apply Function や variant keyframe があれば variants/ が要る。
const hasCustomVariants = Object.keys(rig.variants).length > 1
const hasVariantOnApply = Object.values(rig.variants).some(v =>
Boolean(v.on_apply_function?.trim())
)
const hasVariantKeyframes = animations.some(a =>
a.frames.some(f => (f.variants?.length ?? 0) > 0)
)
const needsVariantFunctions = hasCustomVariants || hasVariantOnApply || hasVariantKeyframes

const variables = {
relativePathToSrc,
blueprint_id: aj.blueprint_id,
Expand Down Expand Up @@ -631,6 +643,7 @@ const dataPackCompiler: DataPackCompiler = async ({
.length > 0,
has_cameras: Object.values(rig.nodes).filter(n => n.type === 'camera').length > 0,
has_animations: animations.length > 0,
needs_variant_functions: needsVariantFunctions,
getNodeTags,
BONE_TYPES,
project_storage: `${aj.blueprint_id}`,
Expand Down
36 changes: 27 additions & 9 deletions src/systems/datapackCompiler/mcbCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Compiler, VariableMap } from 'mc-build/mcl/Compiler'
import { Parser } from 'mc-build/mcl/Parser'
import { TemplateRegisterer } from 'mc-build/mcl/TemplateRegisterer'
import { Tokenizer } from 'mc-build/mcl/TokenizerImpl'
import * as NodePath from 'node:path'
import { getMisodeVersion } from '../minecraft/versionManager'
import type { ExportedFile } from '../util'

Expand All @@ -11,6 +12,16 @@ interface CompilerOptions {
variables: Record<string, any>
version: string
exportedFiles: Map<string, ExportedFile>
/**
* Data Pack format version. 省略時は `version` から misode の版数データを取得する。
* headless なテストからはネットワークアクセスを避けるため明示的に渡す。
*/
formatVersion?: number
/**
* 進捗ログを抑制する。 テストで `variables` 全体が毎回 dump されるのを避けるために使う
* (rig / animations を含むため 1 回で数百 KB になる)。
*/
quiet?: boolean
}

export async function compileMcbProject({
Expand All @@ -19,13 +30,18 @@ export async function compileMcbProject({
variables,
version,
exportedFiles,
formatVersion,
quiet = false,
}: CompilerOptions) {
console.group('Compiling', sourceFiles)
console.log('Variables:', variables)
if (!quiet) {
console.group('Compiling', sourceFiles)
console.log('Variables:', variables)
}

TemplateRegisterer.register()

const misodeVersionData = await getMisodeVersion(version)
const resolvedFormatVersion =
formatVersion ?? (await getMisodeVersion(version)).data_pack_version

const compiler = new Compiler('src', {
libDir: null,
Expand All @@ -37,23 +53,23 @@ export async function compileMcbProject({
ioThreadCount: null,
dontEmitComments: true,
setup: null,
formatVersion: misodeVersionData.data_pack_version,
formatVersion: resolvedFormatVersion,
})
compiler.disableRequire = true

compiler.io = {
cleanup: () => undefined,
finished: () => true,
write: (localPath, content) => {
const writePath = PathModule.join(destPath, localPath)
const writePath = NodePath.join(destPath, localPath)
exportedFiles.set(writePath, {
content,
includeInAJMeta: true,
})
},
}

console.time('MC-Build compiled in')
if (!quiet) console.time('MC-Build compiled in')

const mcbTemplateFiles = Object.entries(sourceFiles).filter(([path]) => path.endsWith('.mcbt'))
const mcbFiles = Object.entries(sourceFiles).filter(([path]) => path.endsWith('.mcb'))
Expand All @@ -77,9 +93,11 @@ export async function compileMcbProject({
}

compiler.compile(VariableMap.fromObject(variables))
console.timeEnd('MC-Build compiled in')
console.log('Exported files:', exportedFiles.keys())
console.groupEnd()
if (!quiet) {
console.timeEnd('MC-Build compiled in')
console.log('Exported files:', exportedFiles.keys())
console.groupEnd()
}

return exportedFiles
}
Loading
Loading