# -*- coding: utf-8 -*- """救护车一次性生成器 (v3,支持多车型): 对 CARS 里的每个车型: 1) 依据其源模型(用户原始模型,已按网易规则做过 X 镜像)逐立方体做层级变换, 得到世界空间 AABB。关键:网易方块实体的渲染映射为 world = (-geo_x/16, geo_y/16, geo_z/16), 即 X 轴镜像;因此碰撞检测必须使用镜像后的世界坐标。 2) 对 4 个车体旋转角 φ(0/90/180/270) 分别求「每格与车体的交集 AABB」,量化到 1/QUANT 格并去重; 几何体根骨骼 Y 角设为 90 - φ(才能得到世界旋转 φ),并把整车居中到主方块中心、车底贴地。 3) 生成:每个车型一份预旋转几何体(4 条,按朝向)、4 个主方块 + 若干形状共享占位方块; 汇总生成 blocks.json、terrain_texture.json,并把布局数据写入 ambulanceServer.py 末尾。 车型命名规则(tag): 主方块 dn_sa:_main_ 占位方块 dn_sa:_ph_ (形状共享,仅提供碰撞) 几何体 geometry.dn_sa__ -> resource_pack/models/entity/_geo.json client_entity entity/_main__block_entity.json """ import json import math import os import io ROOT = r"e:\MCStudioDownload\work\3027246179@qq.com\Cpp\AddOn\0aaaa4f2f16c46e8964025a5ab0ecc02" BP = os.path.join(ROOT, "behavior_pack_kcH4M5u0") RP = os.path.join(ROOT, "resource_pack_t7eJjrxY") BEH_BLOCKS_DIR = os.path.join(BP, "netease_blocks") ENTITY_DIR = os.path.join(RP, "entity") MODELS_DIR = os.path.join(RP, r"models\entity") # ---------------------------------------------------------------- 车型配置 # tag 决定所有资源的命名;src 是源模型;tex 是该车的整车贴图(client_entity 用) CARS = [ { "item": "dn_sa:ambulance_car", "tag": "ambulance_car", "src": os.path.join(ROOT, "_gen_amb_src.geo.json"), "tex": "textures/blocks/ambulance_car", }, { "item": "dn_sa:ambulance_car_broken", "tag": "ambulance_car_broken", "src": os.path.join(ROOT, "_gen_amb_src_broken.geo.json"), "tex": "textures/blocks/ambulance_broken", }, { "item": "dn_sa:ambulance_car_destroyed", "tag": "ambulance_car_destroyed", "src": os.path.join(ROOT, "_gen_amb_src_destroyed.geo.json"), "tex": "textures/blocks/ambulance_destroyed", }, ] # 模型原点(0,0,0) 对应的主方块格上的位置(最小角)。若游戏内整车整体偏移,改这里即可。 ANCHOR = (0.0, 0.0, 0.0) QUANT = 8 # 碰撞盒量化精度:1/8 格(约 2 个贴图像素) YAWS = (0, 90, 180, 270) # 车体在世界空间的旋转角 φ(几何体根骨骼 Y 角 = 90 - φ) # 玩家朝向象限(0=南+Z,90=西-X,180=北-Z,270=东+X) -> 车体世界旋转角 φ # 依据:φ=0 时车头(capo)朝 -X;把 -X 经 Ry(φ) 转到玩家朝向 EXTRA_BY_FACING = {0: 90, 90: 0, 180: 270, 270: 180} # 「车体局部格」(φ=0 坐标系,相对主方块的偏移) —— 这些格子不生成占位方块。 # 1) 挡风玻璃前上方 / 引擎盖正上方那一片:玻璃是斜的(rot -60),它的整体包围盒把它前上方 # 那几格也算了进去,但那里几乎全是空气,碰撞盒会悬在车外。 # 2) 引擎盖最前面(x=-3) 顶层(y=2):该层只有车头那 0.3 格高的一小条,比它后面一格 # (x=-2 顶层已被剔除) 高出来一截,看起来像引擎盖前端多出一块,一并剔除。 # 3) 警灯上方(x=-1) 顶层(y=3):驾驶室顶只有 2.79 高、警灯顶 3.02,这一层(3.0~3.5)全部 # 来自斜挡风玻璃包围盒的溢出,正好悬在警灯上方,看起来像警灯位置高出来一块。 SKIP_LOCAL = { (-2, 2, -1), (-2, 2, 0), (-2, 2, 1), (-2, 3, -1), (-2, 3, 0), (-2, 3, 1), (-3, 2, -1), (-3, 2, 0), (-3, 2, 1), (-1, 3, -1), (-1, 3, 0), (-1, 3, 1), } BLANK_MODEL = "dn_sa:ambulance_blank_model" # 主方块碰撞盒模式(网易方块实体的模型可见性跟着「方块所在位置」走,主方块碰撞盒太小时, # 玩家把屏幕转开、方块位置离开视野,整车的实体模型就会停止渲染而消失): # "model":给主方块一个涵盖整车的碰撞盒(解决上述消失问题) # "cell" :只用主方块自己那一格的碰撞盒(旧行为,其余碰撞靠占位方块) MAIN_BOX_MODE = "model" def model_box(bones, anchor): """整车在世界空间(相对主方块)的包围盒 (lo, hi)。""" boxes = world_boxes(bones, False) # 含零厚度面片,才能真的涵盖整个模型 lo = [min(bx[0][i] for bx in boxes) + anchor[i] for i in range(3)] hi = [max(bx[1][i] for bx in boxes) + anchor[i] for i in range(3)] return lo, hi def skip_cells(phi): """把车体局部格按 yaw 旋转成世界格(与几何体同向)。""" out = set() for (x, y, z) in SKIP_LOCAL: if phi == 0: ax, az = x, z elif phi == 90: ax, az = z, -x elif phi == 180: ax, az = -x, -z else: ax, az = -z, x out.add((ax, y, az)) return out # ---------------------------------------------------------------- math def mat_mul(a, b): return [[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)] def rot_axis(axis, deg): t = math.radians(deg) c, s = math.cos(t), math.sin(t) if axis == 'x': return [[1, 0, 0], [0, c, -s], [0, s, c]] if axis == 'y': return [[c, 0, s], [0, 1, 0], [-s, 0, c]] return [[c, -s, 0], [s, c, 0], [0, 0, 1]] def euler(rx, ry, rz): m = rot_axis('y', ry) m = mat_mul(m, rot_axis('x', rx)) m = mat_mul(m, rot_axis('z', rz)) return m def apply(m, v, pivot): d = [v[i] - pivot[i] for i in range(3)] return [sum(m[i][k] * d[k] for k in range(3)) + pivot[i] for i in range(3)] def cube_corners(cube): o = cube['origin'] s = cube['size'] pt = cube.get('pivot', [0, 0, 0]) pts = [[o[0] + s[0] * dx, o[1] + s[1] * dy, o[2] + s[2] * dz] for dx in (0, 1) for dy in (0, 1) for dz in (0, 1)] rot = cube.get('rotation') if rot and any(abs(v) > 1e-9 for v in rot): m = euler(rot[0], rot[1], rot[2]) pts = [apply(m, p, pt) for p in pts] return pts def bone_world(by_name, bone, p): b = bone while b is not None: rot = b.get('rotation') if rot and any(abs(v) > 1e-9 for v in rot): m = euler(rot[0], rot[1], rot[2]) p = apply(m, p, b.get('pivot', [0, 0, 0])) b = by_name.get(b.get('parent')) return p def world_boxes(bones, skip_degenerate=True): by_name = {b['name']: b for b in bones} out = [] for b in bones: for cube in b.get('cubes', []): pts = [bone_world(by_name, b, p) for p in cube_corners(cube)] # 网易方块实体:世界 X = -几何体 X(镜像),Y/Z 不变 pts = [[-p[0] / 16.0, p[1] / 16.0, p[2] / 16.0] for p in pts] lo = [min(p[i] for p in pts) for i in range(3)] hi = [max(p[i] for p in pts) for i in range(3)] if skip_degenerate and (hi[0] - lo[0] < 1e-9 or hi[1] - lo[1] < 1e-9 or hi[2] - lo[2] < 1e-9): continue # 零厚度面片不产生碰撞 out.append((lo, hi)) return out def cells_for(boxes, anchor): cells = {} for lo, hi in boxes: lo = [lo[i] + anchor[i] for i in range(3)] hi = [hi[i] + anchor[i] for i in range(3)] for ix in range(int(math.floor(lo[0])), int(math.floor(hi[0] - 1e-9)) + 1): for iy in range(int(math.floor(lo[1])), int(math.floor(hi[1] - 1e-9)) + 1): for iz in range(int(math.floor(lo[2])), int(math.floor(hi[2] - 1e-9)) + 1): key = (ix, iy, iz) l = [max(lo[0], ix), max(lo[1], iy), max(lo[2], iz)] h = [min(hi[0], ix + 1), min(hi[1], iy + 1), min(hi[2], iz + 1)] if key not in cells: cells[key] = [l, h] else: pl, ph = cells[key] cells[key] = [[min(pl[i], l[i]) for i in range(3)], [max(ph[i], h[i]) for i in range(3)]] return cells def q(v, quant): return round(v * quant) / float(quant) def local_aabb(cell, l, h, quant=QUANT): """返回该格内 0~1 的局部 AABB(量化 1/quant),退化则返回 None。""" lo = [min(1.0, max(0.0, q(l[i] - cell[i], quant))) for i in range(3)] hi = [min(1.0, max(0.0, q(h[i] - cell[i], quant))) for i in range(3)] if any(hi[i] - lo[i] <= 1e-6 for i in range(3)): return None return tuple(lo + hi) # ---------------------------------------------------------------- per-car 计算 def load_model(path): with io.open(path, 'r', encoding='utf-8') as f: raw = json.load(f) geo = raw['minecraft:geometry'][0] root = [b['name'] for b in geo['bones'] if 'parent' not in b][0] return geo, root def bones_for_yaw(base_geo, root_name, phi): """车体世界旋转 φ 时对应的几何体骨骼:根骨骼 Y=90-φ,并把整车居中到主方块中心、车底贴地。""" bones = json.loads(json.dumps(base_geo['bones'])) for b in bones: if b['name'] == root_name: r = list(b.get('rotation', [0, 0, 0])) b['rotation'] = [r[0], r[1] - phi, r[2]] # 求世界包围盒(已含 X 镜像,含零厚度面片),据此算居中/贴地平移量 boxes = world_boxes(bones, False) lo = [min(bx[0][i] for bx in boxes) for i in range(3)] hi = [max(bx[1][i] for bx in boxes) for i in range(3)] cx = (lo[0] + hi[0]) / 2.0 cz = (lo[2] + hi[2]) / 2.0 tw = (0.5 - cx, -lo[1], 0.5 - cz) # 期望世界平移:X/Z 居中,车底 y=0 shift = [-16.0 * tw[0], 16.0 * tw[1], 16.0 * tw[2]] # 几何体空间平移 Δ = 16*diag(-1,1,1)*tw for b in bones: p = b.get('pivot', [0, 0, 0]) b['pivot'] = [p[i] + shift[i] for i in range(3)] for c in b.get('cubes', []): o = c['origin'] c['origin'] = [o[i] + shift[i] for i in range(3)] if 'pivot' in c: # 立方体自带旋转轴心也需平移,否则非纯平移 cp = c['pivot'] c['pivot'] = [cp[i] + shift[i] for i in range(3)] return bones def wjson(path, obj): with io.open(path, 'w', encoding='utf-8') as f: f.write(json.dumps(obj, indent=2, ensure_ascii=False)) f.write(u"\n") def block_json(identifier, aabb): lo, hi = aabb return { "format_version": "1.10.0", "minecraft:block": { "description": { "identifier": identifier, "register_to_creative_menu": False, "is_experimental": False, "category": "dn_sa", }, "components": { "netease:solid": {"value": True}, "netease:render_layer": {"value": "alpha"}, "minecraft:destroy_time": {"value": 1.0}, "minecraft:block_light_absorption": {"value": 0}, "netease:aabb": { "collision": {"min": list(lo), "max": list(hi)}, "clip": {"min": list(lo), "max": list(hi)}, }, }, }, } # 清理可能残留的旧文件(旧命名 / 旧的占位方块 / 旧的渲染锚点方块与客户端实体) for f in os.listdir(BEH_BLOCKS_DIR): if not f.endswith(".json"): continue if f == "ambulance_car_main.json" or ("ambulance_car" in f and "_ph_" in f): os.remove(os.path.join(BEH_BLOCKS_DIR, f)) for f in os.listdir(ENTITY_DIR): if not f.endswith(".json"): continue if "ambulance_car" in f and f.endswith("_block_entity.json"): os.remove(os.path.join(ENTITY_DIR, f)) old_ent = os.path.join(ENTITY_DIR, "ambulance_car_main_block_entity.json") if os.path.exists(old_ent): os.remove(old_ent) results = [] for car in CARS: tag = car["tag"] main_prefix = "dn_sa:%s_main" % tag ph_prefix = "dn_sa:%s_ph_" % tag geo_id = "geometry.dn_sa_%s" % tag base_geo, root_name = load_model(car["src"]) per_yaw = {} # yaw -> {cell: local_aabb}(逐格碰撞盒) yaw_bones = {} # yaw -> 已旋转/居中/贴地的骨骼 main_box = {} # yaw -> 整车包围盒(主方块「涵盖整个模型」碰撞盒) shapes = {} # local_aabb -> None(形状共享) for yaw in YAWS: bones = bones_for_yaw(base_geo, root_name, yaw) yaw_bones[yaw] = bones cells = cells_for(world_boxes(bones), ANCHOR) skip = skip_cells(yaw) table = {} removed = [] for cell, (l, h) in sorted(cells.items()): if cell in skip: removed.append(cell) continue la = local_aabb(cell, l, h) if la is None: continue table[cell] = la per_yaw[yaw] = table main_box[yaw] = model_box(bones, ANCHOR) for la in table.values(): shapes.setdefault(la, None) print("[%s] yaw=%3d cells=%3d shapes=%3d removed=%s" % (tag, yaw, len(table), len(set(table.values())), removed)) # 形状共享占位方块命名(每种「格内碰撞盒」一个变体,被多格复用) names = {} for i, shape in enumerate(sorted(shapes.keys())): names[shape] = "%s%02d" % (ph_prefix, i) # 每格最终方块名:主方块所在格用主方块,其余用形状共享占位方块 cellname = {} for yaw in YAWS: m = {} for cell, la in per_yaw[yaw].items(): if cell == (0, 0, 0): continue m[cell] = names[la] cellname[yaw] = m print("[%s] placeholder shapes=%d" % (tag, len(names))) # 1) 预旋转几何体:每个朝向一条,渲染整车 geos = [] for yaw in YAWS: desc = dict(base_geo["description"]) desc["identifier"] = "%s_%d" % (geo_id, yaw) geos.append({"description": desc, "bones": yaw_bones[yaw]}) wjson(os.path.join(MODELS_DIR, "%s_geo.json" % tag), {"format_version": "1.21.0", "minecraft:geometry": geos}) # 2) client_entity:每个朝向一份,渲染整车 for yaw in YAWS: name = "%s_%d" % (main_prefix, yaw) ent = { "format_version": "1.10.0", "minecraft:client_entity": { "description": { "identifier": name, "geometry": {"default": "%s_%d" % (geo_id, yaw)}, "textures": {"default": car["tex"]}, "materials": {"default": "entity_alphatest"}, "render_controllers": [ {"controller.render.dn_sa_ambulance_car_main": "variable.variant == 0"} ], "scripts": {"initialize": ["variable.variant = 0;"]}, } }, } wjson(os.path.join(ENTITY_DIR, "%s_main_%d_block_entity.json" % (tag, yaw)), ent) # 3) 方块定义:主方块(带 block_entity) + 形状共享占位方块 for yaw in YAWS: name = "%s_%d" % (main_prefix, yaw) if MAIN_BOX_MODE == "model": lo, hi = main_box[yaw] # 一个碰撞盒涵盖整车 else: la = per_yaw[yaw].get((0, 0, 0)) or (0.0, 0.0, 0.0, 1.0, 1.0, 1.0) lo, hi = la[:3], la[3:] obj = block_json(name, (lo, hi)) obj["minecraft:block"]["components"]["netease:block_entity"] = {"tick": False, "movable": False} wjson(os.path.join(BEH_BLOCKS_DIR, "%s_main_%d.json" % (tag, yaw)), obj) for shape, name in sorted(names.items(), key=lambda kv: kv[1]): short = name.split(":", 1)[1] wjson(os.path.join(BEH_BLOCKS_DIR, short + ".json"), block_json(name, (shape[:3], shape[3:]))) results.append({ "item": car["item"], "tag": tag, "tex": car["tex"], "main_prefix": main_prefix, "ph_prefix": ph_prefix, "cellname": cellname, "names": names, }) print("[%s] wrote geo + %d client_entity + %d block defs" % (tag, len(YAWS), len(YAWS) + len(names))) # ---------------------------------------------------------------- blocks.json blocks = {"format_version": [1, 1, 0]} blocks["dn_sa:generator"] = { "sound": "stone", "client_entity": { "identifier": "dn_sa:generator", "hand_model_use_client_entity": True, "destroyed_textures": "dn_sa:generator_icon", "block_icon": "dn_sa:generator_icon", }, } blocks["dn_sa:salvage_bin"] = { "sound": "stone", "client_entity": { "identifier": "dn_sa:salvage_bin", "hand_model_use_client_entity": True, "destroyed_textures": "dn_sa:salvage_bin_icon", "block_icon": "dn_sa:salvage_bin_icon", }, } for car in results: for yaw in YAWS: name = "%s_%d" % (car["main_prefix"], yaw) blocks[name] = { "sound": "stone", "client_entity": { "identifier": name, "hand_model_use_client_entity": True, "destroyed_textures": car["item"], "block_icon": car["item"], }, } for car in results: for shape, name in sorted(car["names"].items(), key=lambda kv: kv[1]): blocks[name] = {"sound": "stone", "netease_model": BLANK_MODEL} wjson(os.path.join(RP, "blocks.json"), blocks) print("wrote blocks.json (%d entries)" % len(blocks)) # ---------------------------------------------------------------- terrain_texture.json tt_path = os.path.join(RP, r"textures\terrain_texture.json") with io.open(tt_path, 'r', encoding='utf-8') as f: tt = json.load(f) td = tt["texture_data"] for legacy in ("dn_sa_ambulance_car_main", "dn_sa_ambulance_car_placeholder"): td.pop(legacy, None) for key in [k for k in td if k.startswith("dn_sa:ambulance") and "_ph_a" in k]: td.pop(key, None) # 清掉上一次生成的渲染锚点贴图条目 for car in results: td["dn_sa_%s" % car["tag"]] = {"textures": car["tex"]} # 方块破坏/图标贴图 for yaw in YAWS: td["%s_%d" % (car["main_prefix"], yaw)] = {"textures": "textures/blocks/dn_sa_blank"} wjson(tt_path, tt) print("wrote terrain_texture.json") # ---------------------------------------------------------------- 注入碰撞数据到 ambulanceServer.py MARKER = u"# ==== AUTO-GENERATED LAYOUT DATA (by _gen_amb.py, do not edit) ====" lines = [] lines.append(u"") lines.append(MARKER) lines.append(u"# 玩家朝向象限(0=南+Z,90=西-X,180=北-Z,270=东+X) -> 车体额外 Y 旋转角") lines.append(u"EXTRA_BY_FACING = {") for k in sorted(EXTRA_BY_FACING): lines.append(u" %d: %d," % (k, EXTRA_BY_FACING[k])) lines.append(u"}") lines.append(u"# 物品名 -> 车型数据") lines.append(u"CARS = {}") for car in results: lines.append(u"CARS['%s'] = {" % car["item"]) lines.append(u" 'main_prefix': '%s'," % car["main_prefix"]) lines.append(u" 'ph_prefix': '%s'," % car["ph_prefix"]) lines.append(u" 'main': {") for yaw in YAWS: lines.append(u" %d: '%s_%d'," % (yaw, car["main_prefix"], yaw)) lines.append(u" },") lines.append(u" 'cells': {") for yaw in YAWS: lines.append(u" %d: {" % yaw) for cell in sorted(car["cellname"][yaw].keys()): if cell == (0, 0, 0): continue lines.append(u" (%d, %d, %d): '%s'," % (cell[0], cell[1], cell[2], car["cellname"][yaw][cell])) lines.append(u" },") lines.append(u" },") lines.append(u"}") data_block = u"\n".join(lines) server_path = os.path.join(BP, r"Script_dn_sa\ambulanceServer.py") with io.open(server_path, 'r', encoding='utf-8') as f: content = f.read() idx = content.find(MARKER) if idx < 0: raise RuntimeError("marker not found in ambulanceServer.py") with io.open(server_path, 'w', encoding='utf-8') as f: f.write(content[:idx] + data_block + u"\n") print("injected layout data into ambulanceServer.py") old_layout = os.path.join(BP, r"Script_dn_sa\ambulanceLayout.py") if os.path.exists(old_layout): os.remove(old_layout) print("DONE")