あやかしランブル!(Ayakashi)解包求教

就是用 luojun1 大大的脚本解析成文本后再一行行匹配。

我只是用不上文本。文本跟语音的映射键是一样的,带参数的配有语音。

在luojun1大大的脚本的基础上我和游戏播放效果对比猜测得到了如下信息:
新增指令映射:
-18: “比较栈顶值与给定值”,
-17: “(选项)写入栈顶”,
-10: “设置下一条指令位置”,
-8: “栈顶为给定值跳转指定地址”, #一般跟一个-18指令和偏移量
-7: “弹出(清空)栈顶”,
-6: “设置脚本继续与否的标志”,

以及转换为我自己写的播放脚本格式的部分代码,其中注释什么的可能和真实参数有区别,以变量命名为准,很多参数也没有猜出来有什么用,不过基本上涵盖了绝大多数播放代码,用来播放效果已经很接近了:

def convert_commands(commands, evsc_data):
“”“打印命令列表(包含中文指令解释)”“”
if not commands:
print(“\n===== 没有命令 =====”)
return

print("\n===== 命令列表 =====")
print(f"共 {len(commands)} 条命令:")
print("-" * 150)
print(f"{'索引':<5} {'偏移':<10} {'类型':<5} {'调试偏移':<10} {'参数':<100}")
print("-" * 150)

is_unknown_instruction = False
files = []
processes = []
instructions = []
instruction = []

for i, cmd in enumerate(commands):
    if cmd.param.instruction:
        is_unknown_instruction = False
        if len(instruction) > 0:
            instructions.append(instruction)
        # 关键修改:用指令编号减30作为字典的键
        mapped_key = cmd.param.instruction.no - 30
        if mapped_key not in INSTRUCTION_EXPLANATIONS:
            is_unknown_instruction = True
            instruction = []
            continue
        instruction = []
        instruction.append(cmd.data_offset)
        instruction.append(mapped_key)
    elif not is_unknown_instruction:
        if cmd.param.integer:
            instruction.append(cmd.param.integer.value)
        elif cmd.param.float_val:
            instruction.append(cmd.param.float_val.value)
        elif cmd.param.string:
            str_val = evsc_data._get_string_from_pool(cmd.param.string.string_offset)
            instruction.append(str_val)
        elif cmd.param.stack_variable:
            instruction.append(cmd.param.stack_variable.variable_offset)
        else:
            instruction.append(f"0x{cmd.param.data:X}")
if len(instruction) > 0:
    instructions.append(instruction)

time = 0
text = []
textIndex = 0
show_stack = []
wait_jump_list = []
for i, instruction in enumerate(instructions):
    offset_t = instruction.pop(0)
    for j in wait_jump_list:
      if j["payload"]["to"] == offset_t - header_size:
        j["payload"]["to"] = len(processes)
    wait_jump_list = list(filter(lambda x: x["payload"]["to"] != len(processes), wait_jump_list))
    if len(instruction) == 0 or instruction[0] not in INSTRUCTION_EXPLANATIONS:
        continue
    explain = INSTRUCTION_EXPLANATIONS[instruction[0]]
    # 一种一种处理了
    if instruction[0] == 0: # 无操作(NOP)
        continue
    elif instruction[0] == -6: # 设置脚本继续与否的标志
        continue
    elif instruction[0] == -7: # 清理栈顶/丢弃临时值 (pop discard)
        processes.append({
            'time': time,
            'type': "clear_stack",
            'payload':{
                'param': instruction[1],
            }
        })
    elif instruction[0] == -8: # 比较跳转
        print(instruction)
        print(instructions[i+1])
        if instructions[i+1][1] == -18:
            ele = {
                'time': time,
                'type': "compare_jump",
                'payload':{
                    'stack_position': instructions[i+1][2],
                    'value': instructions[i+1][3],
                    'to': instructions[i+1][4]
                }
            }
            processes.append(ele)
            wait_jump_list.append(ele)

    elif instruction[0] == -10: # 跳转
        ele = {
            'time': time,
            'type': "jump",
            'payload':{
                'to': instruction[1]
            }
        }
        processes.append(ele)
        wait_jump_list.append(ele)
    elif instruction[0] == -17: # 写入栈顶
        ele = {
            'time': time,
            'type': "write_stack",
            'payload':{
                'stack_position': instruction[1]
            }
        }
        processes.append(ele)
    elif instruction[0] == -18: # 比较栈顶值与给定值
        continue
    elif instruction[0] == 1: # 读取消息
        # 字符串(偏移: 888, 值: 'アスカ') 名字
        # 字符串(偏移: 900, 值: 'ご主人!$nやっと着いたっスよ!$n陰陽寮っス!') 内容
        # 整数: 23
        # 整数: 0
        # 整数: 9100092 语音
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "show_message",
            'payload':{
                'name': textIndex,
                'content': textIndex,
                'speed': instruction[3],
                'param4': instruction[4],
                'voiceId': instruction[5],
            },
            'blocking': True
        })
        text.append([instruction[1], instruction[2]])
        textIndex+=1
    elif instruction[0] == 2: # 等待工作
        time += instruction[1]
    elif instruction[0] == 3: # 打开章节
        # 60 0 不清楚
        continue
    elif instruction[0] == 4: # 关闭章节
        # 40 0 不清楚,应该就是等这么多时间
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "close_chapter",
            'payload': {
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
        continue
    elif instruction[0] == 5: # 角色删除
        # 9002 删除元素
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "delete_character",
            'payload': {
                'id': instruction[1]
            }
        })
    elif instruction[0] == 6: # 角色显示注册
        # 整数: 9003 id
        # 整数: 9003 人物id advcharacter9003
        # 整数: 1    人物表情 advcharacter9003facial0001
        # 整数: 1    四个参数确定左中右(012)
        # 整数: 0
        # 整数: 3
        if len(instruction) < 7:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        ele = {
            'time': time,
            'type': "show_character",
            'payload': {
                'id': instruction[1],
                'characterId': instruction[2],
                'facialId': instruction[3],
                'position': instruction[4],  # 0: 左, 1: 中, 2: 右
                'param5': instruction[5],
                'param6': instruction[6]
            }
        }
        processes.append(ele)
        show_stack.append(ele)
    elif instruction[0] == 7: # 角色动作跳转
        # 9002 15 8 1 -》表情变换的同时进行一些动作 这个是跳了一下
        if len(instruction) < 5:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "character_action_jump",
            'payload': {
                'id': instruction[1],
                'dy': instruction[2],
                'duration': instruction[3],
            }
        })
        if instruction[4] == 0:
            time += instruction[3]
    elif instruction[0] == 8: # 文件预加载
        # 预加载文件,通常是背景或角色
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        files.append([instruction[1], instruction[2], instruction[3]])
    elif instruction[0] == 9: # 角色移动
        # 9003 0 -10 10 0 向下移动一点然后回原位 第二三个参数我位移距离和方向(x和y轴) 第四个参数代表时间 推测最后一个惨表示是否回原位
        # 9003 0 -20 8 1 向下移动一点然后不回原位
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "move_character",
            'payload': {
                'id': instruction[1],
                'dx': instruction[2],
                'dy': instruction[3],
                'duration': instruction[4]
            }
        })
        if instruction[5] == 0:
            time += instruction[4]
    elif instruction[0] == 10: # 显示执行
        # 0 0 0 -》不知道 ; 生成时间 ;是否异步(1的时候不会影响time)
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        for ele in show_stack:
            ele["time"] = time
            ele["payload"]['duration'] = instruction[2]
        show_stack.clear()
        if instruction[3] == 0:
            time += instruction[2]
    elif instruction[0] == 11: # 角色退出
        # 9003 4 0 -》id 中间可能是时间 第三个参数表示是否异步(1的时候不会影响time)
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "exit_character",
            'payload': {
                'id': instruction[1],
                'duration': instruction[2]
            }
        })
        if instruction[3] == 0:
            time += instruction[2]
    elif instruction[0] == 12: # 角色滑动
        # 整数: 9002
        # 整数: 20 感觉是滑动的距离
        # 整数: 3
        # 整数: 5
        # 整数: 1
        # 推测为左右震动
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "slide_character",
            'payload': {
                'id': instruction[1],
                'distance': instruction[2],
                'times': instruction[3],
                'duration': instruction[4]
            }
        })
        if instruction[5] == 0:
            time += instruction[4] * instruction[3]
    elif instruction[0] == 13: # 白场淡入
        # 20 0 整个场景去白 第一个参数为时间
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_in",
            'payload': {
                'color': "white",  # 白色淡入
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 14: # 白场淡出
        # 20 0 整个场景变白
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_out",
            'payload': {
                'color': "white",  # 白色淡入
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 15: # 黑场淡入
        # 30 0 同上
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_in",
            'payload': {
                'color': "black",  # 黑色淡入
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 16: # 黑场淡出
        # 30 0
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_out",
            'payload': {
                'color': "black",  # 黑色淡出
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 17: # 震动效果
        # 第一个参数0代表整个背景1代表对话框 第二个参数0代表上下1代表左右,第三个参数是振幅 第四个参数应该是时间/次数 第五个参数应该是次数/时间 第六个参数我是否反方向运动 (0表示反方向也要动,反方向也算一次运动)
        # 1 0 30 2 5 1 对话框上下震荡
        if len(instruction) < 7:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        # todo times和duration具体谁是谁还是不确定
        processes.append({
            'time': time,
            'type': "shake",
            'payload': {
                'target': "dialog" if instruction[1] == 1 else "background",
                'direction': "y" if instruction[2] == 0 else "x",
                'distance': instruction[3],
                'times': instruction[4],
                'duration': instruction[5],
            }
        })
        if instruction[6] == 0:
            time += instruction[5] * instruction[4]
    elif instruction[0] == 18: # 相机缩放重置
        # 0 0 代表立刻
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "reset_camera",
            'payload': {
                "duration": instruction[1]  # 立即重置
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 19: # 相机缩放
        # -200 -50 2 0 0 前两个为距离中心点的左边 x y , 第三个参数代表缩放倍数, 后两个参数代表如何变换, 0 表示无过度 0 表示消耗时间
        # 原点为中心
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "zoom_camera",
            'payload': {
                'dx': instruction[1],
                'dy': instruction[2],
                'scale': instruction[3],
                'duration': instruction[4]
            }
        })
        if instruction[5] == 0:
            time += instruction[4]
    elif instruction[0] == 20: # 停止音效(SE)
        # 100003 -》10003音效停止
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "stop_se",
            'payload': {
                'id': instruction[1]
            }
        })
    elif instruction[0] == 21: # 播放音效(SE)
        # 100003 0 -》10003音效 渐变时间
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "play_se",
            'payload': {
                'id': instruction[1],
                'duration': instruction[2]
            }
        })
        time += instruction[2]
    elif instruction[0] == 22: # 停止背景音乐(BGM)
        # 60 -》渐变消失的时间?
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "stop_bgm",
            'payload': {
                'duration': instruction[1]
            }
        })
        time += instruction[1]
    elif instruction[0] == 23: # 播放背景音乐(BGM)
        # 30002 60 对应30002的引用,60不清楚干什么
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "play_bgm",
            'payload': {
                'id': instruction[1],
                'duration': instruction[2]
            }
        })
        time += instruction[2]
    elif instruction[0] == 24: # 背景显示注册
        # 138 -> 准备显示 adventure/advbg/advbg0138.assetbundle作为背景
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        ele = {
            'time': time,
            'type': "show_background",
            'payload': {
                'id': instruction[1],
            }
        }
        processes.append(ele)
        show_stack.append(ele)
    elif instruction[0] == 25: # 分支显示
        # 两个字符串,为空只有一个结果
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "branch_show",
            'payload': {
                'option1': textIndex,
                'option2': textIndex
            },
            'blocking': True
        })
        text.append([instruction[1], instruction[2]])
        textIndex+=1

    elif instruction[0] == 26: # 分支结果
        # 分支结果 todo
        print(f"指令 {i}:{instruction}({explain}) 未处理,跳过")
        pass
    elif instruction[0] == 27: # 消息窗口显示切换
        # 0 -》0隐藏 1显示
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "toggle_message_window",
            'payload': {
                'show': instruction[1] == 1
            }
        })
    elif instruction[0] == 28: # 表情切换
        # todo
        print(f"指令 {i}:{instruction}({explain}) 未处理,跳过")
        pass
    elif instruction[0] == 29: # 播放影片
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "play_movie",
            'payload': {
                'characterId': instruction[1],
                'sceneId': instruction[2]
            }
        })
        pass
    elif instruction[0] == 30: # 对话亮度调整
        # 9002 0 0
        # 0 0 9003 应该是三个参数代表左中右,为0的表示该位置的变暗
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "character_brightness",
            'payload': {
                'left': 1 if instruction[1] > 0 else 0.3,
                'middle': 1 if instruction[2] > 0 else 0.3,
                'right': 1 if instruction[3] > 0 else 0.3
            }
        })
    elif instruction[0] == 31: # 颜色淡入(H)
        # 10 1 0 为从左往右的黑色消失 todo
        # 15 1 0
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_in_horizon",
            'payload': {
                'color': "black" if instruction[2] == 1 else "white",  # 黑色淡入
                'duration': instruction[1],
            }
        })
        if instruction[3] == 0:
            time += instruction[1]
    elif instruction[0] == 32: # 颜色淡出(H)
        # 整数: 15 代表到遮住的时间,同理上面的代表完全显示出来的时间
        # 整数: 1
        # 整数: 0
        # 10 1 0 为从左往右的黑色(最后遮住全屏)
        # 15 1 0
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "fade_out_horizon",
            'payload': {
                'color':"black" if instruction[2] == 1 else "white",  # 黑色淡出
                'duration': instruction[1],
                "param2": instruction[2]
            }
        })
        if instruction[3] == 0:
            time += instruction[1]
    elif instruction[0] == 33: # 文件加载
        # 0为advcharacter  0 9002 0 -》  ui/standing_chara/full/full_chara9002.assetbundle
        # 0为advcharacter  0 9002 1 -》  adventure/advcharacter/advcharacter9002.assetbundle
        # 1为advbg 1 118 1 -》  adventure/advbg/advbg0118.assetbundle
        # 1为advbg 1 118 0 -》  adventure/advbg/advbg0118.assetbundle
        # 3为静态adv场景(9000 2 -》adventure/advstill/advstill9000/still9000_scene02.assetbundle)
        # 4为音效sound/se/adv/ 4 100004 1 -> 首先根据第二个参数作为id去SoundSeMasterDatas中找,然后根据对应的参数和formatId去SoundSeFormatMasterDatas中找
        # 5为sound/voice/adv/ 5 9100022 1 -> 首先根据第二个参数作为id去SoundVoiceMasterDatas中找,然后根据对应的参数和formatId去SoundVoiceFormatMasterDatas中找
        # 6为加载se 6 2 0 -> ss/adv/adveffect0002.assetbundle
        # 7为加载音乐 7 30001 1 -> 首先根据第二个参数作为id去SoundBgmMasterDatas中找,然后根据对应的参数和formatId去SoundBgmFormatMasterDatas中找
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        files.append([instruction[1], instruction[2], instruction[3]])
    elif instruction[0] == 34: # 播放特效动画
        # 无法播放,忽略
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        print(f"指令 {i} ({explain}) 无法播放特效动画,跳过")
    elif instruction[0] == 35: # 停止影片
        if len(instruction) < 1:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "stop_movie",
            'payload': {

            }
        })
        pass
    elif instruction[0] == 36: # 设置阿尔法遮罩
        # 9002 2 1 0 1 为9002元素设置遮罩 其他应该是遮罩的设置 todo
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "set_alpha_mask",
            'payload': {
                'id': instruction[1],
                'param2': instruction[2],
                'param3': instruction[3],
                'param4': instruction[4]
            }
        })
    elif instruction[0] == 37: # 黑框开启
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "black_frame_on",
            'payload': {
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
          time += instruction[1]
    elif instruction[0] == 38: # 黑框关闭
        # todo
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "black_frame_off",
            'payload': {
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
          time += instruction[1]
    elif instruction[0] == 39: # 暗效果开启
        # 15 0 -> 第一个应该是变换时间,效果为整个场景变暗
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "dark_effect_on",
            'payload': {
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 40: # 暗效果关闭
        # 15 0 -> 第一个应该是变换时间,效果为整个场景恢复原样
        if len(instruction) < 3:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "dark_effect_off",
            'payload': {
                'duration': instruction[1]
            }
        })
        if instruction[2] == 0:
            time += instruction[1]
    elif instruction[0] == 41: # 静态画面删除
        # todo
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        print(f"指令 {i}:{instruction}({explain}) 未处理,跳过")
        pass
    elif instruction[0] == 42: # 静态画面显示注册
        # 9000 9000 2 1 -》adventure/advstill/advstill9000/still9000_scene02.assetbundle
        if len(instruction) < 5:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        ele = {
            'time': time,
            'type': "register_static_scene",
            'payload': {
                'id': instruction[1],
                'stillId': instruction[2],
                'sceneId': instruction[3],
                'param4': instruction[4]
            }
        }
        processes.append(ele)
        show_stack.append(ele)
    elif instruction[0] == 43: # 静态画面退出
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        ele = {
            'time': time,
            'type': "delete_static_scene",
            'payload': {
                'id': instruction[1],
                'param2': instruction[2],
                'param3': instruction[3],
            }
        }
        processes.append(ele)
        pass
    elif instruction[0] == 47: # 角色退出并删除
        # 9003 4 0 -》id 中间可能是时间 第三个参数表示是否异步(1的时候不会影响time)
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "exit_and_delete_character",
            'payload': {
                'id': instruction[1],
                'duration': instruction[2]
            }
        })
        if instruction[3] == 0:
            time += instruction[2]
    elif instruction[0] == 48: # 角色相对移动
        # 9003 0 -10 10 0 向下移动一点然后回原位 第二三个参数我位移距离和方向(x和y轴) 第四个参数代表时间 推测最后一个惨表示是否回原位
        # 9003 0 -20 8 1 向下移动一点然后不回原位
        if len(instruction) < 6:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "relative_move_character",
            'payload': {
                'id': instruction[1],
                'dx': instruction[2],
                'dy': instruction[3],
                'duration': instruction[4]
            }
        })
        if instruction[5] == 0:
            time += instruction[4]
    elif instruction[0] == 49: # 读取消息窗口
        if len(instruction) < 7:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "show_message",
            'payload':{
                'name': textIndex,
                'content': textIndex,
                'speed': instruction[3],
                'param4': instruction[4],
                'voiceId': instruction[5],
            },
            'blocking': True
        })
        text.append([instruction[1], instruction[2]])
        textIndex+=1
        if instruction[6] == 0:
            time += instruction[4]
    elif instruction[0] == 56: # 播放动作影片
        if len(instruction) < 4:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "play_movie",
            'payload': {
                'characterId': instruction[1],
                'sceneId': instruction[2]
            }
        })
    elif instruction[0] == 58: # 切换消息窗口
        # 0 -》0隐藏 1显示
        if len(instruction) < 2:
            print(f"指令 {i} ({explain}) 参数不足,跳过")
            continue
        processes.append({
            'time': time,
            'type': "toggle_message_window",
            'payload': {
                'show': instruction[1] == 1
            }
        })
    # todo
    else:
        print(f"指令 {i}:{instruction}({explain}) 未处理,跳过")
result = {
    'files':files,
    'processes':processes,
    'text':text
}
return result

前30条好像是系统自带的,30后的是自定义,所以才减去30。实际脚本中用的好像都是30后的指令。

涉及到分支的地方用到了我增加的那些指令,还好有你这个解析脚本,不然不知道到怎么从evsc文件获取播放信息

我看了下,我写逻辑的时候没有注意这么多,省略了镜头缩放、角色移动什么的(主要是没对比不清楚),就保证了基础的显示播放啥的。

大佬太厉害了,我都难以想象怎么从这二进制文件搞出来这个解析脚本的,我也是用你这个脚本对照着游戏里的播放效果一个个猜的参数,还好有你的INSTRUCTION_EXPLANATIONS,不然光个值猜也没法猜了

老哥你好,请问您的这个解析脚本效果怎么样,可以分享一下吗?

基本上就是上边发的那些,很多参数也没猜出来

參考37樓推測,映射键可能是AdvUserInstructionNo的Enum值。

应该就是了

我用前端写的播放器大概逻辑是这样:

export class FramePlayer {
  public events: ScenePlayProcess[] = []
  public text: [string,string][] = []
  public currentState:{
    character: [{
      id: any;
    },{},{}]
    bg:number,
    camera:{
      x:number,
      y:number,
      scale:number
    },
    se: [],
    bgm: number,
    dialog:{
      name:string,
      content:string,
      voicePath:string,
    }
  } =
    {
      character: [{

      },{},{}],
      bg:0,
      camera:{
        x:0,
        y:0,
        scale:1
      },
      se:[],
      bgm: 0,
      dialog: {
        name: "",
        content: "",
        voicePath: ""
      }
  }
  public end:boolean = false
  public canPlay:boolean = false
  public stack_template = []
  public stack = [0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  public seInfo = {}
  public lastNow = 0
  public voiceInfo = {}
  public bgmInfo = {}
  public urls: string[] = []
  public fps: number = 30
  public speedUpMultiplier: number = 2
  public frameTime: number = 1000 / this.fps
  public speedUpFrameTime: number = 1000 / this.fps / this.speedUpMultiplier
  public commonFrameTime: number = 1000 / this.fps
  public playing = false
  public skip = false
  public speedUp = false
  public auto = false
  public log: [string,string][] = []
  public state:[] = [] // 存储每次阻塞时的静态状态用于跳转,比较复杂
  public elements: PlayerElements = {}
  public elementsState: AnimationStateObj = {}
  public cursor = 0              // 事件指针
  public logicTime = 0           // 逻辑时间
  public raf = 0
  public breakpoint = ref<number|null>(null)

  constructor(processes: Processes, focus:boolean = false) {

    try{
      if(!processes){
        ElMessage.error("没有传入合理处理数据");
        throw new Error("没有传入合理处理数据");
      }
      let files = processes.files
      files.forEach((file)=>{
        if(file[0] == 0){
          this.urls.push(AyarabuManagement.ABUrlFormatter.charaStandFull.format(file[1]))
          this.urls.push(AyarabuManagement.ABUrlFormatter.charaAdvIllu.format(file[1]))
        }
        else if (file[0] == 1) {
          this.urls.push(AyarabuManagement.ABUrlFormatter.advBG.format(file[1]))
        }
        else if (file[0] == 2) {
          let characterId = file[1]
          let sceneId = file[2]
          this.urls.push(AyarabuManagement.ABUrlFormatter.charaSceneMovie.format(characterId,sceneId))
        }
        else if (file[0] == 3) {
          this.urls.push(AyarabuManagement.ABUrlFormatter.charaAdvStill.format(file[1], file[2]))
        }
        else if (file[0] == 4) {
          // {
          //   "soundSeId": 100004,
          //   "formatId": 100001,
          //   "argument1": 4,
          //   "argument2": 0,
          //   "loop": false
          // }
          let soundSeMasterData = AyarabuManagement.getMockDataById(AyarabuManagement.MockFileName.SoundSeMasterDatas,"soundSeId",file[1])
          if(soundSeMasterData){
            // {
            //   "seFormatId": 100001,
            //   "directoryPath": "adv",
            //   "assetBundleName": "sescenario_{1:D4}",
            //   "assetDataName": "SeScenario_{1:D4}"
            // }
            let soundSeFormatMasterData = AyarabuManagement.getMockDataById(AyarabuManagement.MockFileName.SoundSeFormatMasterDatas,"seFormatId",soundSeMasterData.formatId)
            if(soundSeFormatMasterData["assetBundleName"]){
              this.urls.push(`sound/se/${soundSeFormatMasterData["directoryPath"]}/${soundSeFormatMasterData["assetBundleName"].format(
                soundSeMasterData.argument1, soundSeMasterData.argument2,
              )}.assetbundle`)
            }
            this.seInfo[file[1]] = {
              path:`${AyarabuManagement.resourceSoundPath}/se/${soundSeFormatMasterData["directoryPath"]}/${soundSeFormatMasterData["assetDataName"].format(
                soundSeMasterData.argument1, soundSeMasterData.argument2,
              )}.m4a`,
              loop: soundSeMasterData.loop
            }
          }else {
            this.seInfo[file[1]] = {
              path:``,
              loop: false
            }
          }
        }
        else if (file[0] == 5) {
          // {
          //   "soundVoiceId": 9100022,
          //   "formatId": 9100001,
          //   "argument1": 0,
          //   "argument2": 0
          // }
          let soundVoiceMasterData = AyarabuManagement.getMockJsonData(AyarabuManagement.MockFileName.SoundVoiceMasterDatas, 1)[file[1]]
          if(!soundVoiceMasterData){
            this.voiceInfo[file[1]] = ``
          }else {
            // {
            //   "voiceFormatId": 9100001,
            //   "directoryPath": "adv/asuka/common",
            //   "assetBundleName": "vocommon_asuka_{1:D2}",
            //   "assetDataName": "VoCommon_Asuka_{1:D2}"
            // }
            let soundVoiceFormatMasterData = AyarabuManagement.getMockJsonData(AyarabuManagement.MockFileName.SoundVoiceFormatMasterDatas, 1)[soundVoiceMasterData.formatId]
            let directoryPath = soundVoiceFormatMasterData["directoryPath"].format(soundVoiceMasterData.argument1, soundVoiceMasterData.argument2)
            let assetBundleName = soundVoiceFormatMasterData["assetBundleName"].format(soundVoiceMasterData.argument1, soundVoiceMasterData.argument2)
            let assetDataName = soundVoiceFormatMasterData["assetDataName"].format(soundVoiceMasterData.argument1, soundVoiceMasterData.argument2)
            if(soundVoiceFormatMasterData["assetBundleName"]){
              this.urls.push(`sound/voice/${directoryPath}/${assetBundleName}.assetbundle`)
            }

            this.voiceInfo[file[1]] = `${AyarabuManagement.resourceSoundPath}/voice/${directoryPath}/${assetDataName}.m4a`
          }
        }
        else if (file[0] == 6) {
          this.urls.push(`ss/adv/adveffect{1:D4}.assetbundle`.format(file[1]))
        }
        else if (file[0] == 7) {
          // {
          //   "soundBgmId": 10002,
          //   "formatId": 10001,
          //   "argument1": 2,
          //   "argument2": 0,
          //   "loop": true
          // }
          let soundBgmMasterData = AyarabuManagement.getMockDataById(AyarabuManagement.MockFileName.SoundBgmMasterDatas,"soundBgmId",file[1])
          // {
          //   "bgmFormatId": 10001,
          //   "directoryPath": "ui",
          //   "assetBundleName": "bgmpage_{1:D4}",
          //   "assetDataName": "BgmPage_{1:D4}"
          // }
          if(!soundBgmMasterData){
            soundBgmMasterData = AyarabuManagement.getMockDataById(AyarabuManagement.MockFileName.SoundBgmMasterDatas,"soundBgmId",10001)
          }
          let soundBgmFormatMasterData = AyarabuManagement.getMockDataById(AyarabuManagement.MockFileName.SoundBgmFormatMasterDatas,"bgmFormatId",soundBgmMasterData.formatId)
          if(soundBgmFormatMasterData["assetBundleName"]){
            this.urls.push(`sound/bgm/${soundBgmFormatMasterData["directoryPath"]}/${soundBgmFormatMasterData["assetBundleName"].format(
              soundBgmMasterData.argument1, soundBgmMasterData.argument2,
            )}.assetbundle`)
          }
          this.bgmInfo[file[1]] = `${AyarabuManagement.resourceSoundPath}/bgm/${soundBgmFormatMasterData["directoryPath"]}/${soundBgmFormatMasterData["assetDataName"].format(
            soundBgmMasterData.argument1, soundBgmMasterData.argument2,
          )}.m4a`

        }
        else {
          console.log("无法解析");
        }
      })
      this.urls = [...new Set(this.urls)]
      AyarabuManagement.downloadFiles(this.urls, null, true, focus, ()=>{
        this.canPlay = true
      })
      // AyarabuManagement.downloadFile("adventure/advcharacter/advcharacter9001.assetbundle",true)
      // AyarabuManagement.downloadFile("ui/standing_chara/full/full_chara9001.assetbundle",true)

      this.events = processes.processes   // 第二步的预处理
      this.text = processes.text
      this.text.forEach(text=>{
        text[0] = text[0].replaceAll("$n","\n").replaceAll("<name></name>",useGlobalStore().userName)
        text[1] = text[1].replaceAll("$n","\n").replaceAll("<name></name>",useGlobalStore().userName).replace(/<[^>]*name[^>]*>/g, useGlobalStore().userName);
      })
      useGlobalStore().eventPlayer = this

    } catch (error){
      console.log(error.message);
      console.log(error.stack);
    }

  }

  play() {
    this.playing = true
    if(this.raf){
      cancelAnimationFrame(this.raf)
      this.raf = 0
    }
    this.lastNow = performance.now() / this.commonFrameTime
    useGlobalStore().components.textDialog.nextTextShow = false
    this.raf = requestAnimationFrame(this.tick)
  }

  playBranch(branchId:number) {
    // if(useGlobalStore().components.textDialog.showChoices){
    //   let validateChoiceCount = useGlobalStore().components.textDialog.branchText.filter(e=>e).length;
    //   if(validateChoiceCount > 1){
    //     useGlobalStore().components.textDialog.showChoices = false
    //     this.play()
    //   }else {
    //     this.stack_template.push(branchId)
    //   }
    // }
    this.stack_template.push(branchId)
    useGlobalStore().components.textDialog.showChoices = false
    this.play()
  }

  pause() {
    this.playing = false
    cancelAnimationFrame(this.raf)
  }

  skipToNextBreakpoint() {
    const next = this.events.findIndex(
      (ev, i) => i > this.cursor && ev.blocking
    )
    this.jump(next === -1 ? this.events.length : next)
  }

  jump(cursor: number) {
    this.cursor = cursor
    this.logicTime = this.events[cursor]?.time ?? Infinity
    // 全局 rollback + apply 到 logicTime
    // rollbackToTime(this.logicTime)
    this.play()
  }
  private applyEvent(ev, index = 0){
    this.log.push([ev.type, JSON.stringify(ev.payload)])
    let duration = 0
    let path = ""
    let position = 0
    let color = "black"
    let seId = -1
    let stillId = -1
    let sceneId = -1
    let bgId
    let characterId
    let to
    let stack_position
    let value
    let text
    switch (ev.type) {
      case "write_stack": // 写入栈
        stack_position = ev.payload.stack_position
        if(this.stack_template.length > 0){
          this.stack[this.stack.length + stack_position] = this.stack_template.pop()
        }
        break;
      case "jump": // 直接跳转
        to = ev.payload.to
        this.cursor = to
        this.logicTime =  this.events[this.cursor].time
        break;
      case "compare_jump": // 比较跳转
        stack_position = ev.payload.stack_position
        value = ev.payload.value
        to = ev.payload.to
        if(this.stack[this.stack.length + stack_position] == value){
          this.cursor = to
          this.logicTime =  this.events[this.cursor].time
        }
        break;
      case "clear_stack": // 清空栈
        for (let i = 0; i < this.stack.length; i++) {
          this.stack[i] = 0;
        }
        break;
      case "show_message": // 读取消息
        let text = this.text[ev.payload.name]
        useGlobalStore().components.textDialog.speaker = text[0]
        useGlobalStore().components.textDialog.showContent(text[1], this.speedUp, ev.payload.speed*this.frameTime)
        this.currentState.dialog.name = text[0]
        this.currentState.dialog.content = text[1]
        if(ev.payload.voiceId > 0){
          let voicePath = AyarabuManagement.getIosnotOrR18PathIfNotExist(this.voiceInfo[ev.payload.voiceId])
          useGlobalStore().components.textDialog.voicePath = voicePath
          this.currentState.dialog.voicePath = voicePath
        }
        useGlobalStore().components.textDialog.dialogRefShow = true
        break;
      case 2: // 等待工作
        break;
      case 3: // 打开章节
        break;
      case "close_chapter": // 关闭章节
        ElMessage.success("章节已结束")
        this.end = true
        break;
      case "delete_character": // 角色删除
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        if(position > -1){
          useGlobalStore().components.eventPlay[`characterPath${position}`] = ""
          this.currentState.character[position]["id"] = 0
          this.currentState.character[position]["characterId"] = 0
          this.currentState.character[position]["facialId"] = 0
          this.currentState.character[position]["path"] = ''
          this.currentState.character[position]["opacity"] = 0
        }
        break; // 无需进行具体的操作
      case "show_character": // 角色显示注册
        path = `${AyarabuManagement.resourceAdvcharacterPath}/advcharacter{1:D4}/advcharacter{1:D4}facial{2:D4}.png`.format(
          ev.payload.characterId, ev.payload.facialId
        )
        let path2 = AyarabuManagement.getIosnotOrR18PathIfNotExist(path)
        if(path2 == null){
          path = `${AyarabuManagement.resourceAdvcharacterPath}/advcharacter{1:D4}/advcharacter{1:D4}.png`.format(ev.payload.characterId)
          path = AyarabuManagement.getIosnotOrR18PathIfNotExist(path)
        } else {
          path = path2
        }
        this.currentState.character[ev.payload.position]["id"] = ev.payload.id
        this.currentState.character[ev.payload.position]["characterId"] = ev.payload.characterId
        this.currentState.character[ev.payload.position]["facialId"] = ev.payload.facialId
        this.currentState.character[ev.payload.position]["path"] = path
        this.currentState.character[ev.payload.position]["opacity"] = 0
        useGlobalStore().components.eventPlay.characterBrightness[ev.payload.position] = 1
        useGlobalStore().components.eventPlay[`characterPath${ev.payload.position}`] = path
        nextTick(()=>{
          if((this.currentState.camera.y !=0 || this.currentState.camera.x !=0) && ev.payload.position == 1){
            this.elements[`character${ev.payload.position}`].value.playAnimation([animationUtils.animationNames.opacity],[
              [0,1]
            ],ev.payload.duration*this.frameTime/1000, 0)
          }else {
            this.elements[`character${ev.payload.position}`].value.playAnimation([animationUtils.animationNames.opacity,animationUtils.animationNames.x,animationUtils.animationNames.y],[
              [0,1],
              [0],
              [0]
            ],ev.payload.duration*this.frameTime/1000, 0)
          }
        })
        break;
      case "character_action_jump": // 角色动作跳转
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        duration = ev.payload.duration ?? 0
        if(position == -1){
          if(this.currentState.character[1]["id"] != 0){
            position = 1
          }else {
            break
          }
        }
        this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.y],[
          [0,ev.payload.dy,0],
        ],ev.payload.duration*this.frameTime/1000, 0)
        break;
      case "move_character": // 角色移动
        console.log(this.logicTime, ev);
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        if(position > -1){
          let dx = ev.payload.dx ?? 0
          let dy = ev.payload.dy ?? 0
          duration = ev.payload.duration ?? 0
          this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.x,animationUtils.animationNames.y],[
            [0,`calc(${30 - position * 30}% - ${dx}px)`],
            [0,-dy],
          ],duration*this.frameTime/1000, 0, true)
        }
        break;
      case "relative_move_character": // 角色相对移动
        console.log(this.logicTime, ev);
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        if(position > -1){
          let dx = ev.payload.dx ?? 0
          let dy = ev.payload.dy ?? 0
          duration = ev.payload.duration ?? 0
          this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.x,animationUtils.animationNames.y],[
            [0,dx],
            [0,-dy],
          ],duration*this.frameTime/1000, 0, false, true)
        }
        break;
      case "exit_character": // 角色退出
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        if(position > -1){
          duration = ev.payload.duration ?? 0
          this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.opacity],[
            [1,0],
          ],duration*this.frameTime/1000, 0)
          this.currentState.character[position]["id"] = 0
          this.currentState.character[position]["characterId"] = 0
          this.currentState.character[position]["facialId"] = 0
          this.currentState.character[position]["path"] = ''
          this.currentState.character[position]["opacity"] = 0
        }
        break;
      case "exit_and_delete_character": // 角色退出并删除
        if(ev.payload.id == 0){
          for(position in this.currentState.character){
            useGlobalStore().components.eventPlay[`characterPath${position}`] = ""
            this.currentState.character[position]["id"] = 0
            this.currentState.character[position]["characterId"] = 0
            this.currentState.character[position]["facialId"] = 0
            this.currentState.character[position]["path"] = ''
            this.currentState.character[position]["opacity"] = 0
          }
        }else {
          position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
          if(position > -1){
            duration = ev.payload.duration ?? 0
            this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.opacity],[
              [1,0],
            ],duration*this.frameTime/1000, 0)
            useGlobalStore().components.eventPlay[`characterPath${position}`] = ""
            this.currentState.character[position]["id"] = 0
            this.currentState.character[position]["characterId"] = 0
            this.currentState.character[position]["facialId"] = 0
            this.currentState.character[position]["path"] = ''
            this.currentState.character[position]["opacity"] = 0
          }
        }
        break;
      case "slide_character": // 角色滑动
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.id)
        if(position > -1){
          let duration = ev.payload.duration ?? 0
          let dx = ev.payload.distance ?? 0
          let times = ev.payload.times ?? 0
          this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.x],[
            [0,dx,0,-dx,0],
          ],duration*this.frameTime/1000, times - 1)
        }
        break;
      case "fade_in": // 淡入
        color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        if(color in colors){
          this.elements.fade.value.playAnimation([animationUtils.animationNames.background, animationUtils.animationNames.opacity], [
            [`radial-gradient(circle, ${colors[color].toRgba(1)}, ${colors[color].toRgba(1)})`, `radial-gradient(circle, ${colors[color].toRgba(0)}, ${colors[color].toRgba(1)})`],
            [1, 0]
          ], duration * this.frameTime / 1000, 0)
        }
        break;
      case "fade_out": // 淡出
        color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        if(color in colors){
          this.elements.fade.value.playAnimation([animationUtils.animationNames.background, animationUtils.animationNames.opacity], [
            [`radial-gradient(circle, ${colors[color].toRgba(0)}, ${colors[color].toRgba(1)})`, `radial-gradient(circle, ${colors[color].toRgba(1)}, ${colors[color].toRgba(1)})`],
            [0, 1]
          ], duration * this.frameTime / 1000, 0)
        }
        break;
      case "shake": // 震动效果
        if(ev.payload.target == "dialog"){
          let distance = ev.payload.distance
          let times = ev.payload.times
          duration = ev.payload.duration ?? 0
          useGlobalStore().components.textDialog.dialogRef.playAnimation([ev.payload.direction],[
            [0,distance/2,0,-distance/2,0],
          ],duration*this.frameTime/1000, times-1)
        }else {
          let distance = ev.payload.distance
          let times = ev.payload.times
          duration = ev.payload.duration ?? 0
          this.elements.bg.value.playAnimation([ev.payload.direction], [
            [0, distance / 2, 0, -distance / 2, 0]
          ], duration * this.frameTime / 1000, times - 1)
        }
        break;
      case "reset_camera": // 重置摄像机
        duration = ev.payload.duration ?? 0
        this.elements.bg.value.playAnimation([animationUtils.animationNames.x, animationUtils.animationNames.y, animationUtils.animationNames.scale], [
          [this.currentState.camera.x, 0],
          [this.currentState.camera.y, 0],
          [this.currentState.camera.scale, 1]
        ], duration * this.frameTime / 1000, 0)
        this.elements.character1?.value.playAnimation([animationUtils.animationNames.x, animationUtils.animationNames.y, animationUtils.animationNames.scale], [
          [0, 0],
          [0, 0],
          [0, 1]
        ], duration * this.frameTime / 1000, 0, true)
        this.currentState.camera.x = 0
        this.currentState.camera.y = 0
        this.currentState.camera.scale = 1
        break;
      case "zoom_camera": // 摄像机缩放
        duration = ev.payload.duration ?? 0
        let dx = ev.payload.dx ?? 0;
        let dy = ev.payload.dy ?? 0;
        let scale = ev.payload.scale ?? 1
        this.elements.bg.value.playAnimation([animationUtils.animationNames.x, animationUtils.animationNames.y, animationUtils.animationNames.scale], [
          [this.currentState.camera.x, -dx],
          [this.currentState.camera.y, dy],
          [this.currentState.camera.scale, scale]
        ], duration * this.frameTime / 1000, 0)
        this.elements.character1?.value.playAnimation([animationUtils.animationNames.x, animationUtils.animationNames.y, animationUtils.animationNames.scale], [
          [this.currentState.camera.x, -dx],
          [this.currentState.camera.y, this.elements.character1?.value.getElement().clientHeight/385.8*dy],
          [this.currentState.camera.scale, scale]
        ], duration * this.frameTime / 1000, 0)
        this.currentState.camera.x = -dx
        this.currentState.camera.y = dy
        this.currentState.camera.scale = scale
        break;
      case "stop_se": // 停止音效(SE)
        seId = ev.payload.id
        if(this.currentState.se.includes(seId)){
          useGlobalStore().components.eventPlay.sePath = ""
          this.currentState.se = this.currentState.se.filter(id => id !== seId)
        }
        break;
      case "play_se": // 播放音效(SE)
        seId = ev.payload.id
        duration = ev.payload.duration ?? 0
        if(seId in this.seInfo){
          useGlobalStore().components.eventPlay.playSeFadeIn(
            this.seInfo[seId].path,
            duration * this.frameTime / 1000,this.seInfo[seId].loop
          )
          this.currentState.se.push(seId)
        }
        break;
      case "stop_bgm": // 停止背景音乐(BGM)
        duration = ev.payload.duration ?? 0
        if(this.currentState.bgm){
          useGlobalStore().components.eventPlay.playBgmFadeOut(
            duration * this.frameTime / 1000
          )
          this.currentState.bgm = 0
        }
        break;
      case "play_bgm": // 播放背景音乐(BGM)
        let bgmId = ev.payload.id;
        duration = ev.payload.duration ?? 0
        if(bgmId in this.bgmInfo){
          useGlobalStore().components.eventPlay.playBgmFadeIn(
            this.bgmInfo[bgmId],duration * this.frameTime / 1000
          )
          this.currentState.bgm = bgmId
        }
        break;
      case "show_background": // 背景显示注册
        bgId = ev.payload.id;
        duration = ev.payload.duration ?? 0
        if(useGlobalStore().components.eventPlay.previewBackgroundPath != useGlobalStore().components.eventPlay.backgroundPath){
          useGlobalStore().components.eventPlay.previewBackgroundPath = useGlobalStore().components.eventPlay.backgroundPath
        }
        if(duration > 0){
          nextTick(()=>{
            useGlobalStore().components.eventPlay.backgroundPath = AyarabuManagement.LocalResourcePathFormatter.advBG.format(bgId)
            this.elements.bg.value.playAnimation([animationUtils.animationNames.opacity, animationUtils.animationNames.x, animationUtils.animationNames.y], [[0, 1], [0], [0]], duration * this.frameTime / 1000, 0)
          })
        }else {
          useGlobalStore().components.eventPlay.backgroundPath = AyarabuManagement.LocalResourcePathFormatter.advBG.format(bgId)
          this.elements.bg.value.playAnimation([animationUtils.animationNames.opacity, animationUtils.animationNames.x, animationUtils.animationNames.y], [[0, 1], [0], [0]], duration * this.frameTime / 1000, 0)
        }
        this.currentState.bg = bgId
        break;
      case "branch_show": // 分支显示
        useGlobalStore().components.textDialog.branchText = [this.text[ev.payload.option1][0],this.text[ev.payload.option1][1]]
        useGlobalStore().components.textDialog.showChoices = true
        break;
      case 26: // 分支结果
        break;
      case "toggle_message_window": // 消息窗口显示切换
        useGlobalStore().components.textDialog.dialogRefShow = ev.payload.show
        break;
      case "play_movie": // 播放影片
        characterId = ev.payload.characterId
        sceneId = ev.payload.sceneId
        path = AyarabuManagement.getIosnotOrR18PathIfNotExist(AyarabuManagement.LocalResourcePathFormatter.charaSceneMovie.format(characterId,sceneId))

        useGlobalStore().components.eventPlay.moviePath = path
        break;
      case "character_brightness": // 对话亮度调整
        useGlobalStore().components.eventPlay.characterBrightness = [ev.payload.left, ev.payload.middle, ev.payload.right]
        break;
      case "fade_in_horizon": // 颜色淡入(H)
        color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        if(color in colors){
          this.elements.fade.value.playAnimation([animationUtils.animationNames.background], [
            [
              `linear-gradient(to right, ${colors.black.toRgba(1)},0%, ${colors.black.toRgba(1)}`,
              `linear-gradient(to right, ${colors.black.toRgba(0)},100%, ${colors.black.toRgba(1)}`
            ]
          ], duration * this.frameTime / 1000, 0)
        }
        break;
      case "fade_out_horizon": // 颜色淡出(H)
        color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        if(color in colors){
          this.elements.fade.value.playAnimation([animationUtils.animationNames.background], [
            [
              `linear-gradient(to right, ${colors.black.toRgba(0)} 0%, ${colors.black.toRgba(0)}`,
              `linear-gradient(to right, ${colors.black.toRgba(1)} 100%, ${colors.black.toRgba(0)}`
            ]
          ], duration * this.frameTime / 1000, 0)
        }
        break;
      case "stop_movie": // 停止影片
        useGlobalStore().components.eventPlay.moviePath = ""
        useGlobalStore().components.eventPlay.previewMoviePath = ""
        break;
      case "set_alpha_mask": // 设置阿尔法遮罩
        position = this.currentState.character.findIndex(c=> c.id == ev.payload.characterId)
        if (position > -1){
          this.elements[`character${position}`].value.playAnimation([animationUtils.animationNames.mask],[
            [
              [`radial-gradient(circle at center, black,80%, transparent)`],
            ],
          ],0, 0)
        }
        break;
      case "black_frame_on": // 黑框开启
        // color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        this.elements.frame.value.playAnimation(["backgroundImage",animationUtils.animationNames.opacity], [
          ["",`url(${AyarabuManagement.resourceAdventureCommonPath.replaceAll("\\","/")}/black_frame.png)`],
          [0, 1]
        ], duration * this.frameTime / 1000, 0)
        break;
      case "black_frame_off": // 黑框关闭
        // color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        this.elements.frame.value.playAnimation(["backgroundImage",animationUtils.animationNames.opacity], [
          [`url(${AyarabuManagement.resourceAdventureCommonPath.replace("\\","/")}/black_frame.png)`,""],
          [1, 0]
        ], duration * this.frameTime / 1000, 0)
        break;
      case "dark_effect_on": // 暗效果开
        // color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        this.elements.fade.value.playAnimation([animationUtils.animationNames.background, animationUtils.animationNames.opacity], [
          [`radial-gradient(circle, ${colors[color].toRgba(0)}, ${colors[color].toRgba(1)})`, `radial-gradient(circle, ${colors[color].toRgba(0.5)}, ${colors[color].toRgba(0.8)})`],
          [0, 1]
        ], duration * this.frameTime / 1000, 0)
        break;
      case "dark_effect_off": // 暗效果关
        // color = ev.payload.color ?? 'white'
        duration = ev.payload.duration ?? 0
        this.elements.fade.value.playAnimation([animationUtils.animationNames.background, animationUtils.animationNames.opacity], [
          [`radial-gradient(circle, ${colors[color].toRgba(0.5)}, ${colors[color].toRgba(0.8)})`, `radial-gradient(circle, ${colors[color].toRgba(0)}, ${colors[color].toRgba(1)})`],
          [1, 0]
        ], duration * this.frameTime / 1000, 0)
        break;
      case "delete_static_scene": // 静态画面显示删除
        nextTick(()=>{
          if(useGlobalStore().components.eventPlay.moviePath == ""){
            useGlobalStore().components.eventPlay.showFullBackground = false
          }
        })
        break;
      case "register_static_scene": // 静态画面显示注册
        bgId = ev.payload.id;
        stillId = ev.payload.stillId ?? 0
        sceneId = ev.payload.sceneId ?? 0
        duration = ev.payload.duration ?? 0
        useGlobalStore().components.eventPlay.showFullBackground = true
        if(useGlobalStore().components.eventPlay.previewBackgroundPath != useGlobalStore().components.eventPlay.backgroundPath){
          useGlobalStore().components.eventPlay.previewBackgroundPath = useGlobalStore().components.eventPlay.backgroundPath
        }
        path = AyarabuManagement.LocalResourcePathFormatter.charaAdvStill.format(stillId,sceneId)
        if(!fs.existsSync(path)){
          path = AyarabuManagement.LocalResourcePathFormatter.charaR18AdvStill.format(stillId,sceneId)
        }
        if(duration > 0){
          nextTick(()=>{
            useGlobalStore().components.eventPlay.backgroundPath = path
            this.elements.bg.value.playAnimation([animationUtils.animationNames.opacity, animationUtils.animationNames.x, animationUtils.animationNames.y], [[0, 1], [0], [0]], duration * this.frameTime / 1000, 0)
          })
        }else {
          useGlobalStore().components.eventPlay.backgroundPath = path
          this.elements.bg.value.playAnimation([animationUtils.animationNames.opacity, animationUtils.animationNames.x, animationUtils.animationNames.y], [[0, 1], [0], [0]], duration * this.frameTime / 1000, 0)
        }
        this.currentState.bg = bgId
        break;
      default:
        console.warn(`Unknown event type: ${ev.type}`);
    }
  }
  private tick = () => {
    if (!this.playing) return
    const now = performance.now() / this.commonFrameTime
    const dt = now - (this.lastNow ?? now)
    this.lastNow = now

    this.logicTime += this.speedUp ? (dt * this.speedUpMultiplier) : dt

    while (this.cursor < this.events.length && this.events[this.cursor].time <= this.logicTime) {
      const ev = this.events[this.cursor++]
      this.applyEvent(ev)          // 各轨道 reducer
      if (ev.blocking) {
        if(!this.speedUp || ev.type == "branch_show"){
          this.pause()
        }
        break
      }else {

      }
    }

    if (this.cursor >= this.events.length) {
      this.pause()
    } else {
      this.raf = requestAnimationFrame(this.tick)
    }
  }
}

和之前发的解析脚本结合起来然后前端相关元素创建好,赋值进行播放就行,因为第一次做所以代码写得很烂,我倒是想写一个通用的播放器,但是不同游戏各有特点,感觉挺难的

43樓提到前30條是系統指令,確實有個AdvEVSCSystemInstructionNo的Enum,最大值剛好是30。超過30好像會變成AdvUserInstructionNo

如果EVSC Command Type是Instruction,Param Count是指要往下讀取多少個Command(參數)。

跟據Param Count的數值,猜測可能和AdvCommandExecution這個Class裡面的Method有關,但有些Instruction Param Count的值和Method Arguments數量對不起來(基本上會多或會少1個)。

可能因为有的指令是由几个方法共同完成的,所以会对不上,具体的可以看看AdvEventScriptCtrl。不过挺久没看我也忘差不多了。