@erik9994857/cag 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cag +10 -0
- package/defaults/info.cag +4 -0
- package/defaults/main.cag +3 -0
- package/defaults/src/code/example.cagc +5 -0
- package/package.json +33 -0
- package/src/api/api-registry.js +144 -0
- package/src/api/model-api.js +274 -0
- package/src/api/pull-api.js +331 -0
- package/src/api/worldgen-api.js +332 -0
- package/src/cli/cag-cli.js +324 -0
- package/src/index.js +214 -0
- package/src/models/bbmodel-parser.js +320 -0
- package/src/models/uv-mapper.js +315 -0
- package/src/parser/cag-parser.js +241 -0
- package/src/parser/cagc-parser.js +255 -0
- package/src/parser/tokenizer.js +233 -0
- package/src/resources/resource-loader.js +257 -0
- package/src/runtime/context.js +163 -0
- package/src/runtime/executor.js +213 -0
- package/src/runtime/scope.js +198 -0
- package/src/utils/file-resolver.js +208 -0
- package/src/utils/id-validator.js +171 -0
- package/src/utils/logger.js +203 -0
package/bin/cag
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erik9994857/cag",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CaG — A code library and custom language for building 3D worlds with .cagc files, .bbmodel support, and auto UV mapping",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cag": "./bin/cag"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"bin/",
|
|
12
|
+
"defaults/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test/basic.test.js",
|
|
16
|
+
"start": "node src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cag",
|
|
20
|
+
"game-engine",
|
|
21
|
+
"3d",
|
|
22
|
+
"bbmodel",
|
|
23
|
+
"blockbench",
|
|
24
|
+
"voxel",
|
|
25
|
+
"worldgen",
|
|
26
|
+
"custom-language"
|
|
27
|
+
],
|
|
28
|
+
"author": "Erik9994857",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const ModelAPI = require("./model-api");
|
|
2
|
+
const WorldGenAPI = require("./worldgen-api");
|
|
3
|
+
const PullAPI = require("./pull-api");
|
|
4
|
+
|
|
5
|
+
class APIRegistry {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.apis = {};
|
|
8
|
+
this.initialized = false;
|
|
9
|
+
this.registrationOrder = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
initialize(engine) {
|
|
13
|
+
var resourceMap = engine.getResourceMap();
|
|
14
|
+
var info = engine.getInfo();
|
|
15
|
+
var path = require("path");
|
|
16
|
+
|
|
17
|
+
var modelAPI = new ModelAPI(resourceMap);
|
|
18
|
+
this.register("Model.API", modelAPI);
|
|
19
|
+
|
|
20
|
+
var worldGenAPI = new WorldGenAPI(modelAPI);
|
|
21
|
+
this.register("WorldGen.API", worldGenAPI);
|
|
22
|
+
|
|
23
|
+
var resourcesPath = path.join(engine.projectRoot, "src", info.resourcesfolder);
|
|
24
|
+
var pullAPI = new PullAPI(resourceMap, resourcesPath);
|
|
25
|
+
this.register("Pull.API", pullAPI);
|
|
26
|
+
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
register(name, apiInstance) {
|
|
32
|
+
if (this.apis[name]) {
|
|
33
|
+
throw new Error("API already registered: " + name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.apis[name] = {
|
|
37
|
+
name: name,
|
|
38
|
+
instance: apiInstance,
|
|
39
|
+
callCount: 0,
|
|
40
|
+
lastCalled: null
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.registrationOrder.push(name);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get(name) {
|
|
48
|
+
if (!this.apis[name]) {
|
|
49
|
+
throw new Error("API not found: " + name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.apis[name].instance;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
execute(apiName, action, params) {
|
|
56
|
+
if (!this.apis[apiName]) {
|
|
57
|
+
throw new Error("API not registered: " + apiName);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var entry = this.apis[apiName];
|
|
61
|
+
entry.callCount++;
|
|
62
|
+
entry.lastCalled = new Date().toISOString();
|
|
63
|
+
|
|
64
|
+
return entry.instance.execute(action, params);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
has(name) {
|
|
68
|
+
return this.apis[name] !== undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
list() {
|
|
72
|
+
var result = [];
|
|
73
|
+
for (var i = 0; i < this.registrationOrder.length; i++) {
|
|
74
|
+
var name = this.registrationOrder[i];
|
|
75
|
+
var entry = this.apis[name];
|
|
76
|
+
result.push({
|
|
77
|
+
name: name,
|
|
78
|
+
callCount: entry.callCount,
|
|
79
|
+
lastCalled: entry.lastCalled
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getStats() {
|
|
86
|
+
var totalCalls = 0;
|
|
87
|
+
var apiCount = this.registrationOrder.length;
|
|
88
|
+
|
|
89
|
+
for (var i = 0; i < this.registrationOrder.length; i++) {
|
|
90
|
+
totalCalls += this.apis[this.registrationOrder[i]].callCount;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
apiCount: apiCount,
|
|
95
|
+
totalCalls: totalCalls,
|
|
96
|
+
initialized: this.initialized,
|
|
97
|
+
apis: this.list()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
reset() {
|
|
102
|
+
for (var i = 0; i < this.registrationOrder.length; i++) {
|
|
103
|
+
var name = this.registrationOrder[i];
|
|
104
|
+
this.apis[name].callCount = 0;
|
|
105
|
+
this.apis[name].lastCalled = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
unregister(name) {
|
|
110
|
+
if (!this.apis[name]) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
delete this.apis[name];
|
|
115
|
+
|
|
116
|
+
var idx = this.registrationOrder.indexOf(name);
|
|
117
|
+
if (idx !== -1) {
|
|
118
|
+
this.registrationOrder.splice(idx, 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
validateDependencies(dependencies) {
|
|
125
|
+
var missing = [];
|
|
126
|
+
var found = [];
|
|
127
|
+
|
|
128
|
+
for (var i = 0; i < dependencies.length; i++) {
|
|
129
|
+
if (this.has(dependencies[i])) {
|
|
130
|
+
found.push(dependencies[i]);
|
|
131
|
+
} else {
|
|
132
|
+
missing.push(dependencies[i]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
valid: missing.length === 0,
|
|
138
|
+
found: found,
|
|
139
|
+
missing: missing
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = APIRegistry;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
class ModelAPI {
|
|
5
|
+
constructor(resourceMap, defaultModelsPath) {
|
|
6
|
+
this.resourceMap = resourceMap || {};
|
|
7
|
+
this.loadedModels = {};
|
|
8
|
+
this.defaultModelsPath = defaultModelsPath || path.join(__dirname, "..", "models", "defaults");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
execute(action, params) {
|
|
12
|
+
switch (action) {
|
|
13
|
+
case "UseModel":
|
|
14
|
+
return this.useModel(params);
|
|
15
|
+
case "RemoveModel":
|
|
16
|
+
return this.removeModel(params);
|
|
17
|
+
case "ListModels":
|
|
18
|
+
return this.listModels();
|
|
19
|
+
default:
|
|
20
|
+
throw new Error("Unknown Model.API action: " + action);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
useModel(params) {
|
|
25
|
+
if (!params || params.length === 0) {
|
|
26
|
+
throw new Error("UseModel requires a model name parameter");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const modelName = params[0];
|
|
30
|
+
|
|
31
|
+
if (this.loadedModels[modelName]) {
|
|
32
|
+
return this.loadedModels[modelName];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let resource = this.resourceMap[modelName];
|
|
36
|
+
|
|
37
|
+
if (!resource) {
|
|
38
|
+
resource = this.loadDefaultModel(modelName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!resource) {
|
|
42
|
+
throw new Error("Model not found: " + modelName);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const model = this.loadModel(modelName, resource);
|
|
46
|
+
this.loadedModels[modelName] = model;
|
|
47
|
+
return model;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
loadDefaultModel(modelName) {
|
|
51
|
+
const modelPath = path.join(this.defaultModelsPath, modelName.toLowerCase() + ".bbmodel");
|
|
52
|
+
const texturePath = path.join(this.defaultModelsPath, modelName.toLowerCase() + ".png");
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(modelPath)) {
|
|
55
|
+
return {
|
|
56
|
+
model: modelPath,
|
|
57
|
+
texture: fs.existsSync(texturePath) ? texturePath : null,
|
|
58
|
+
paired: fs.existsSync(texturePath),
|
|
59
|
+
isDefault: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loadModel(name, resource) {
|
|
67
|
+
const result = {
|
|
68
|
+
name: name,
|
|
69
|
+
geometry: null,
|
|
70
|
+
texture: null,
|
|
71
|
+
uvMappings: null,
|
|
72
|
+
valid: false
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (resource.model) {
|
|
76
|
+
const modelData = this.parseBBModel(resource.model);
|
|
77
|
+
result.geometry = modelData.geometry;
|
|
78
|
+
result.uvMappings = modelData.uvMappings;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (resource.texture) {
|
|
82
|
+
result.texture = this.loadTexture(resource.texture);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result.geometry && result.texture && result.uvMappings) {
|
|
86
|
+
result.uvMappings = this.validateAndFixUVMappings(result.uvMappings, result.texture);
|
|
87
|
+
result.valid = true;
|
|
88
|
+
} else if (result.geometry) {
|
|
89
|
+
result.valid = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parseBBModel(filePath) {
|
|
96
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
97
|
+
let data;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(raw);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw new Error("Invalid .bbmodel file (not valid JSON): " + filePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const geometry = {
|
|
106
|
+
elements: [],
|
|
107
|
+
bones: [],
|
|
108
|
+
resolution: data.resolution || { width: 16, height: 16 }
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const uvMappings = [];
|
|
112
|
+
|
|
113
|
+
if (data.elements && Array.isArray(data.elements)) {
|
|
114
|
+
for (const element of data.elements) {
|
|
115
|
+
const geomElement = {
|
|
116
|
+
name: element.name || "unnamed",
|
|
117
|
+
from: element.from || [0, 0, 0],
|
|
118
|
+
to: element.to || [1, 1, 1],
|
|
119
|
+
rotation: element.rotation || [0, 0, 0],
|
|
120
|
+
origin: element.origin || [0, 0, 0],
|
|
121
|
+
faces: {}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (element.faces) {
|
|
125
|
+
const faceNames = ["north", "south", "east", "west", "up", "down"];
|
|
126
|
+
for (const faceName of faceNames) {
|
|
127
|
+
if (element.faces[faceName]) {
|
|
128
|
+
const face = element.faces[faceName];
|
|
129
|
+
geomElement.faces[faceName] = {
|
|
130
|
+
uv: face.uv || [0, 0, 16, 16],
|
|
131
|
+
texture: face.texture !== undefined ? face.texture : 0
|
|
132
|
+
};
|
|
133
|
+
uvMappings.push({
|
|
134
|
+
element: geomElement.name,
|
|
135
|
+
face: faceName,
|
|
136
|
+
uv: face.uv || [0, 0, 16, 16],
|
|
137
|
+
textureIndex: face.texture !== undefined ? face.texture : 0
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
geometry.elements.push(geomElement);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (data.outliner && Array.isArray(data.outliner)) {
|
|
148
|
+
geometry.bones = this.parseOutliner(data.outliner);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { geometry, uvMappings };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
parseOutliner(outliner) {
|
|
155
|
+
const bones = [];
|
|
156
|
+
|
|
157
|
+
for (const item of outliner) {
|
|
158
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
159
|
+
const bone = {
|
|
160
|
+
name: item.name || "unnamed",
|
|
161
|
+
origin: item.origin || [0, 0, 0],
|
|
162
|
+
rotation: item.rotation || [0, 0, 0],
|
|
163
|
+
children: []
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (item.children && Array.isArray(item.children)) {
|
|
167
|
+
bone.children = this.parseOutliner(item.children);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
bones.push(bone);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return bones;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
loadTexture(filePath) {
|
|
178
|
+
if (!fs.existsSync(filePath)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
183
|
+
const buffer = fs.readFileSync(filePath);
|
|
184
|
+
let width = 0;
|
|
185
|
+
let height = 0;
|
|
186
|
+
|
|
187
|
+
if (ext === ".png") {
|
|
188
|
+
if (buffer.length >= 24) {
|
|
189
|
+
width = buffer.readUInt32BE(16);
|
|
190
|
+
height = buffer.readUInt32BE(20);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
path: filePath,
|
|
196
|
+
width: width,
|
|
197
|
+
height: height,
|
|
198
|
+
format: ext.substring(1),
|
|
199
|
+
size: buffer.length
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
validateAndFixUVMappings(uvMappings, texture) {
|
|
204
|
+
if (!texture || texture.width === 0 || texture.height === 0) {
|
|
205
|
+
return uvMappings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const fixed = [];
|
|
209
|
+
|
|
210
|
+
for (const mapping of uvMappings) {
|
|
211
|
+
const uv = mapping.uv;
|
|
212
|
+
let needsFix = false;
|
|
213
|
+
|
|
214
|
+
if (uv[0] < 0 || uv[1] < 0 || uv[2] < 0 || uv[3] < 0) {
|
|
215
|
+
needsFix = true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (uv[2] > texture.width || uv[3] > texture.height) {
|
|
219
|
+
needsFix = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (needsFix) {
|
|
223
|
+
const fixedMapping = Object.assign({}, mapping);
|
|
224
|
+
fixedMapping.uv = [
|
|
225
|
+
Math.max(0, Math.min(uv[0], texture.width)),
|
|
226
|
+
Math.max(0, Math.min(uv[1], texture.height)),
|
|
227
|
+
Math.max(0, Math.min(uv[2], texture.width)),
|
|
228
|
+
Math.max(0, Math.min(uv[3], texture.height))
|
|
229
|
+
];
|
|
230
|
+
fixedMapping.wasFixed = true;
|
|
231
|
+
fixed.push(fixedMapping);
|
|
232
|
+
} else {
|
|
233
|
+
fixed.push(mapping);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return fixed;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
removeModel(params) {
|
|
241
|
+
if (!params || params.length === 0) {
|
|
242
|
+
throw new Error("RemoveModel requires a model name parameter");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const modelName = params[0];
|
|
246
|
+
|
|
247
|
+
if (!this.loadedModels[modelName]) {
|
|
248
|
+
throw new Error("Model not loaded: " + modelName);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
delete this.loadedModels[modelName];
|
|
252
|
+
return { removed: modelName };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
listModels() {
|
|
256
|
+
return Object.keys(this.loadedModels).map(function (name) {
|
|
257
|
+
return {
|
|
258
|
+
name: name,
|
|
259
|
+
valid: this.loadedModels[name].valid,
|
|
260
|
+
hasTexture: this.loadedModels[name].texture !== null
|
|
261
|
+
};
|
|
262
|
+
}.bind(this));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getLoadedModel(name) {
|
|
266
|
+
return this.loadedModels[name] || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
isModelLoaded(name) {
|
|
270
|
+
return this.loadedModels[name] !== undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = ModelAPI;
|