diff --git a/CMakeLists.txt b/CMakeLists.txt index 8447a89..d977d64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ target_sources(xsystem4 PRIVATE src/3d/particle.c src/3d/reign.c src/3d/renderer.c + src/3d/s3de.c src/dungeon/dgn.c src/dungeon/dtx.c diff --git a/include/reign.h b/include/reign.h index 6a101f7..6b2d7b0 100644 --- a/include/reign.h +++ b/include/reign.h @@ -84,6 +84,12 @@ struct RE_camera { vec3 pos; float pitch, roll, yaw; // in degrees float quake_pitch, quake_yaw; + // Per-frame full override from an 3de "camera" object. Cleared at the + // start of every RE_build_model tick and re-asserted by s3de_effect_update + // if any active camera object selects this frame. + bool override_active; + vec3 override_pos; + float override_pitch, override_yaw, override_roll; }; struct RE_back_cg { @@ -104,7 +110,7 @@ struct RE_plugin { struct RE_instance **instances; struct archive *aar; struct hash_table *model_cache; - struct hash_table *pae_cache; + struct hash_table *effect_cache; struct RE_renderer *renderer; struct RE_camera camera; mat4 proj_transform; @@ -158,6 +164,7 @@ struct RE_instance { struct RE_plugin *plugin; struct model *model; struct particle_effect *effect; + struct s3de_effect *s3de_effect; struct motion *motion; struct motion *next_motion; struct height_detector *height_detector; diff --git a/src/3d/3d_internal.h b/src/3d/3d_internal.h index 798cdc7..dc2bc32 100644 --- a/src/3d/3d_internal.h +++ b/src/3d/3d_internal.h @@ -402,6 +402,178 @@ void particle_effect_free(struct particle_effect *effect); void particle_effect_update(struct RE_instance *inst); void particle_object_calc_local_transform(struct RE_instance *inst, struct particle_object *po, struct particle_instance *pi, float frame, mat4 dest); +// s3de.c + +// .3de effect system. The 's' prefix is just to avoid an identifier starting +// with a digit; it stands for SealEngine. + +enum s3de_object_type { + S3DE_OBJ_BILLBOARD, + S3DE_OBJ_POLYGON, + S3DE_OBJ_CAMERA, +}; + +enum s3de_move_type { + S3DE_MOVE_LINEAR, + S3DE_MOVE_BEZIER, +}; + +enum s3de_spawn_position_type { + S3DE_SPAWN_NORMAL, + S3DE_SPAWN_SPHERE, + S3DE_SPAWN_HORIZONTAL, +}; + +enum s3de_blend_type { + S3DE_BLEND_NORMAL, + S3DE_BLEND_ADDITIVE, +}; + +enum s3de_emitter_link_type { + S3DE_LINK_NONE = 0, // frozen at spawn-time world pos + S3DE_LINK_FOLLOW = 1, // particle follows current emitter pos + S3DE_LINK_ROTATE_AT_SPAWN = 2, // emitter rotation around spawn pos + S3DE_LINK_ROTATE_AT_CURRENT = 3, // emitter rotation around current pos + S3DE_LINK_MAX = S3DE_LINK_ROTATE_AT_CURRENT +}; + +enum s3de_direction_type { + S3DE_DIR_RANDOM, + S3DE_DIR_SPECIFIED, + S3DE_DIR_EMITTER, + S3DE_DIR_EMITTER_REVERSE, + S3DE_DIR_EMITTER_COORD, + S3DE_DIR_ARBITRARY_PLANE, + S3DE_DIR_SPAWN, +}; + +enum s3de_posture_type { + S3DE_POSE_NONE, + S3DE_POSE_CAMERA, + S3DE_POSE_MOVE_DIR, + S3DE_POSE_MOVE_AND_FLY, + S3DE_POSE_FLY_AND_EMITTER_MOVE, +}; + +enum s3de_interp_type { + S3DE_INTERP_NONE, + S3DE_INTERP_LINEAR, + S3DE_INTERP_SLERP, +}; + +struct s3de_range { + float base; + float random; +}; + +struct s3de_scalar_entry { + int frame; + float value; + enum s3de_interp_type interp; +}; + +struct s3de_vec3_entry { + int frame; + vec3 v; + enum s3de_interp_type interp; +}; + +struct s3de_position_entry { + int frame; + vec3 pos; + enum s3de_interp_type interp; + float spawn_radius; + float spawn_angle_deg; +}; + +struct s3de_object { + char *name; + enum s3de_object_type type; + enum s3de_move_type movement_type; + enum s3de_spawn_position_type spawn_position_type; + float spawn_distance; + int particle_count; + + char *texture; + + enum s3de_blend_type blend_type; + char *polygon_name; + enum s3de_emitter_link_type emitter_link_type; + bool soft_fog_edge; + + enum s3de_posture_type posture_type; + enum s3de_direction_type direction_type; + vec3 direction; + float direction_angle; + + struct s3de_range start_size, end_size; + struct s3de_range start_x_size, end_x_size; + struct s3de_range start_y_size, end_y_size; + struct s3de_range offset_x, offset_y, offset_z; + struct s3de_range speed, acceleration; + struct s3de_range movement_distance, movement_curve; + + bool free_fall; + float mass, air_resistance; + + struct s3de_range start_x_rotation, end_x_rotation; + struct s3de_range start_y_rotation, end_y_rotation; + struct s3de_range start_z_rotation, end_z_rotation; + + float x_revolution_angle[2], y_revolution_angle[2], z_revolution_angle[2]; + float x_revolution_distance[2], y_revolution_distance[2], z_revolution_distance[2]; + + int child_frame; + float texture_anime_frame; + + struct s3de_range alpha_fade_in_frame, alpha_fade_out_frame; + + int *damages; + int nr_damages; + + struct s3de_position_entry *position_list; + int nr_position_entries; + struct s3de_scalar_entry *size_list; + int nr_size_entries; + struct s3de_scalar_entry *x_size_list; + int nr_x_size_entries; + struct s3de_scalar_entry *y_size_list; + int nr_y_size_entries; + struct s3de_scalar_entry *alpha_list; + int nr_alpha_entries; + struct s3de_vec3_entry *rotation_list; + int nr_rotation_entries; + struct s3de_vec3_entry *multiply_color_list; + int nr_multiply_color_entries; + struct s3de_vec3_entry *additive_color_list; + int nr_additive_color_entries; + + struct model *model; +}; + +struct s3de { + char *path; + int loop_start_frame; + int loop_end_frame; + struct s3de_object *objects; + int nr_objects; +}; + +struct s3de *s3de_load(struct archive *aar, const char *path); +void s3de_free(struct s3de *s); + +struct s3de_effect { + struct s3de *s3de; + int wav_channel; // -1 if no sound + bool sound_started; + float last_frame; +}; + +struct s3de_effect *s3de_effect_create(struct s3de *s, struct archive *aar); +void s3de_effect_free(struct s3de_effect *eff); +void s3de_effect_update(struct RE_instance *inst); +void s3de_calc_frame_range(struct s3de *s, struct motion *motion); + // parser.c struct pol { diff --git a/src/3d/reign.c b/src/3d/reign.c index 4279574..1883bd5 100644 --- a/src/3d/reign.c +++ b/src/3d/reign.c @@ -77,6 +77,10 @@ static void unload_instance(struct RE_instance *instance) particle_effect_free(instance->effect); instance->effect = NULL; } + if (instance->s3de_effect) { + s3de_effect_free(instance->s3de_effect); + instance->s3de_effect = NULL; + } if (instance->bone_transforms) { glDeleteBuffers(1, &instance->bone_transforms_ubo); free(instance->bone_transforms); @@ -238,7 +242,7 @@ struct RE_plugin *RE_plugin_new(enum RE_plugin_version version) plugin->instances = xcalloc(plugin->nr_instances, sizeof(struct RE_instance *)); plugin->aar = aar; plugin->model_cache = ht_create(32); - plugin->pae_cache = ht_create(32); + plugin->effect_cache = ht_create(32); for (int i = 0; i < RE_NR_BACK_CGS; i++) RE_back_cg_init(&plugin->back_cg[i]); plugin->mag_speed = 1; @@ -260,8 +264,9 @@ void RE_plugin_free(struct RE_plugin *plugin) archive_free(plugin->aar); ht_foreach_value(plugin->model_cache, (void(*)(void*))model_free); ht_free(plugin->model_cache); - ht_foreach_value(plugin->pae_cache, (void(*)(void*))pae_free); - ht_free(plugin->pae_cache); + ht_foreach_value(plugin->effect_cache, re_plugin_version < RE_SEAL_PLUGIN ? + (void(*)(void*))pae_free : (void(*)(void*))s3de_free); + ht_free(plugin->effect_cache); for (int i = 0; i < RE_NR_BACK_CGS; i++) RE_back_cg_destroy(&plugin->back_cg[i]); xfree_aligned(plugin); @@ -321,6 +326,7 @@ bool RE_build_model(struct RE_plugin *plugin, int elapsed_ms) return false; plugin->camera.quake_pitch = plugin->camera.quake_yaw = 0.0; + plugin->camera.override_active = false; for (int i = 0; i < plugin->nr_instances; i++) { struct RE_instance *inst = plugin->instances[i]; @@ -342,7 +348,11 @@ bool RE_build_model(struct RE_plugin *plugin, int elapsed_ms) // update_bones(). for (int i = 0; i < plugin->nr_instances; i++) { struct RE_instance *inst = plugin->instances[i]; - if (inst && inst->type == RE_ITYPE_PARTICLE_EFFECT) + if (!inst || inst->type != RE_ITYPE_PARTICLE_EFFECT) + continue; + if (inst->s3de_effect) + s3de_effect_update(inst); + else particle_effect_update(inst); } return true; @@ -469,7 +479,6 @@ bool RE_instance_load(struct RE_instance *instance, const char *name) if (name[0] == '\0') return true; // empty name just unloads the instance - struct pae *pae; switch (instance->type) { case RE_ITYPE_STATIC: case RE_ITYPE_SKINNED: @@ -488,18 +497,34 @@ bool RE_instance_load(struct RE_instance *instance, const char *name) } return !!instance->model; case RE_ITYPE_PARTICLE_EFFECT: - pae = ht_get(instance->plugin->pae_cache, name, NULL); - if (!pae) { - pae = pae_load(instance->plugin->aar, name); - if (!pae) - return false; - ht_put(instance->plugin->pae_cache, name, pae); - } - instance->effect = particle_effect_create(pae); - if (!instance->motion) { - instance->motion = xcalloc(1, sizeof(struct motion)); - instance->motion->instance = instance; - pae_calc_frame_range(instance->effect->pae, instance->motion); + if (re_plugin_version >= RE_SEAL_PLUGIN) { + struct s3de *s3de = ht_get(instance->plugin->effect_cache, name, NULL); + if (!s3de) { + s3de = s3de_load(instance->plugin->aar, name); + if (!s3de) + return false; + ht_put(instance->plugin->effect_cache, name, s3de); + } + instance->s3de_effect = s3de_effect_create(s3de, instance->plugin->aar); + if (!instance->motion) { + instance->motion = xcalloc(1, sizeof(struct motion)); + instance->motion->instance = instance; + s3de_calc_frame_range(instance->s3de_effect->s3de, instance->motion); + } + } else { + struct pae *pae = ht_get(instance->plugin->effect_cache, name, NULL); + if (!pae) { + pae = pae_load(instance->plugin->aar, name); + if (!pae) + return false; + ht_put(instance->plugin->effect_cache, name, pae); + } + instance->effect = particle_effect_create(pae); + if (!instance->motion) { + instance->motion = xcalloc(1, sizeof(struct motion)); + instance->motion->instance = instance; + pae_calc_frame_range(instance->effect->pae, instance->motion); + } } return true; default: @@ -670,7 +695,7 @@ bool RE_instance_find_path(struct RE_instance *inst, vec3 start, vec3 goal) if (!inst || !inst->model->collider) return false; mat4 vp_transform; - RE_calc_view_matrix(&inst->plugin->camera, GLM_YUP, vp_transform); + RE_calc_view_matrix(&inst->plugin->camera, NULL, vp_transform); glm_mat4_mul(inst->plugin->proj_transform, vp_transform, vp_transform); return collider_find_path(inst->model->collider, start, goal, vp_transform); } @@ -701,7 +726,7 @@ bool RE_instance_calc_path_finder_intersect_eye_vec(struct RE_instance *inst, in vec4 far = { ndc_x, ndc_y, 1.f, 1.f }; mat4 inv_vp; - RE_calc_view_matrix(&inst->plugin->camera, GLM_YUP, inv_vp); + RE_calc_view_matrix(&inst->plugin->camera, NULL, inv_vp); glm_mat4_mul(inst->plugin->proj_transform, inv_vp, inv_vp); glm_mat4_inv(inv_vp, inv_vp); @@ -724,7 +749,7 @@ bool RE_plugin_transform_pos_to_view_pos(struct RE_plugin *plugin, float x, floa return false; mat4 vp; - RE_calc_view_matrix(&plugin->camera, GLM_YUP, vp); + RE_calc_view_matrix(&plugin->camera, NULL, vp); glm_mat4_mul(plugin->proj_transform, vp, vp); vec4 pos = { x, y, z, 1.f }; @@ -746,11 +771,16 @@ bool RE_plugin_get_camera_z_vector(struct RE_plugin *plugin, vec3 out) if (!plugin) return false; - vec3 euler = { - glm_rad(plugin->camera.pitch + plugin->camera.quake_pitch), - glm_rad(plugin->camera.yaw + plugin->camera.quake_yaw), - glm_rad(plugin->camera.roll) - }; + vec3 euler; + if (plugin->camera.override_active) { + euler[0] = glm_rad(plugin->camera.override_pitch); + euler[1] = glm_rad(plugin->camera.override_yaw); + euler[2] = glm_rad(plugin->camera.override_roll); + } else { + euler[0] = glm_rad(plugin->camera.pitch + plugin->camera.quake_pitch); + euler[1] = glm_rad(plugin->camera.yaw + plugin->camera.quake_yaw); + euler[2] = glm_rad(plugin->camera.roll); + } mat4 rot; glm_euler_yxz(euler, rot); glm_mat4_mulv3(rot, GLM_FORWARD, 0.f, out); diff --git a/src/3d/renderer.c b/src/3d/renderer.c index 6500d6e..2d3501e 100644 --- a/src/3d/renderer.c +++ b/src/3d/renderer.c @@ -332,15 +332,30 @@ void RE_renderer_free(struct RE_renderer *r) void RE_calc_view_matrix(struct RE_camera *camera, vec3 up, mat4 out) { vec3 front = { 0.0, 0.0, -1.0 }; - vec3 euler = { - glm_rad(camera->pitch + camera->quake_pitch), - glm_rad(camera->yaw + camera->quake_yaw), - glm_rad(camera->roll) - }; + vec3 euler; + float *pos; + if (camera->override_active) { + euler[0] = glm_rad(camera->override_pitch); + euler[1] = glm_rad(camera->override_yaw); + euler[2] = glm_rad(camera->override_roll); + pos = camera->override_pos; + } else { + euler[0] = glm_rad(camera->pitch + camera->quake_pitch); + euler[1] = glm_rad(camera->yaw + camera->quake_yaw); + euler[2] = glm_rad(camera->roll); + pos = camera->pos; + } mat4 rot; glm_euler_yxz(euler, rot); glm_mat4_mulv3(rot, front, 0.0, front); - glm_look(camera->pos, front, up, out); + vec3 up_v; + if (up) { + glm_vec3_copy(up, up_v); // Use the provided up vector. + } else { + // Derive the camera's up by rotating world-Y with the same orientation. + glm_mat4_mulv3(rot, GLM_YUP, 0.0, up_v); + } + glm_look(pos, front, up_v, out); } static bool should_draw_shadow(struct mesh *mesh, struct material *material) @@ -892,7 +907,8 @@ static void render_back_cg(struct texture *dst, struct RE_back_cg *bcg, struct R static void setup_lights(struct RE_plugin *plugin) { struct RE_renderer *r = plugin->renderer; - glUniform3fv(r->camera_pos, 1, plugin->camera.pos); + glUniform3fv(r->camera_pos, 1, plugin->camera.override_active + ? plugin->camera.override_pos : plugin->camera.pos); glUniform3fv(r->global_ambient, 1, plugin->global_ambient); int light_index = 0; for (int i = 0; i < plugin->nr_instances; i++) { @@ -1000,7 +1016,7 @@ void RE_render(struct sact_sprite *sp) glEnable(GL_CULL_FACE); mat4 view_transform; - RE_calc_view_matrix(&plugin->camera, GLM_YUP, view_transform); + RE_calc_view_matrix(&plugin->camera, NULL, view_transform); // Tweak the projection transform so that the rendering result is vertically // flipped. If we render the scene normally, the resulting image will be diff --git a/src/3d/s3de.c b/src/3d/s3de.c new file mode 100644 index 0000000..62a0d41 --- /dev/null +++ b/src/3d/s3de.c @@ -0,0 +1,796 @@ +/* Copyright (C) 2026 kichikuou + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include +#include + +#include + +#include "system4.h" +#include "system4/aar.h" +#include "system4/utfsjis.h" + +#include "3d_internal.h" +#include "audio.h" + +static void parse_error(const char *msg, char *input) +{ + char *eol = strchr(input, '\n'); + if (eol) *eol = '\0'; + WARNING("Parse error: %s Near: \"%s\"", msg, input); +} + +static enum s3de_object_type parse_object_type(const char *s) +{ + if (!strcmp(s, "ビルボード")) + return S3DE_OBJ_BILLBOARD; + if (!strcmp(s, "ポリゴンオブジェクト")) + return S3DE_OBJ_POLYGON; + if (!strcmp(s, "カメラ")) + return S3DE_OBJ_CAMERA; + WARNING("unknown object type \"%s\"", s); + return (enum s3de_object_type)-1; +} + +static enum s3de_move_type parse_move_type(const char *s) +{ + if (!strcmp(s, "直線")) + return S3DE_MOVE_LINEAR; + if (!strcmp(s, "ベジェ")) + return S3DE_MOVE_BEZIER; + WARNING("unknown move type \"%s\"", s); + return (enum s3de_move_type)-1; +} + +static enum s3de_spawn_position_type parse_spawn_position_type(const char *s) +{ + if (!strcmp(s, "標準")) + return S3DE_SPAWN_NORMAL; + if (!strcmp(s, "球")) + return S3DE_SPAWN_SPHERE; + if (!strcmp(s, "水平面")) + return S3DE_SPAWN_HORIZONTAL; + WARNING("unknown spawn position type \"%s\"", s); + return (enum s3de_spawn_position_type)-1; +} + +static enum s3de_blend_type parse_blend_type(const char *s) +{ + if (!strcmp(s, "標準")) + return S3DE_BLEND_NORMAL; + if (!strcmp(s, "加算")) + return S3DE_BLEND_ADDITIVE; + WARNING("unknown blend type \"%s\"", s); + return (enum s3de_blend_type)-1; +} + +static enum s3de_direction_type parse_direction_type(const char *s) +{ + if (!strcmp(s, "ランダム")) + return S3DE_DIR_RANDOM; + if (!strcmp(s, "指定方向")) + return S3DE_DIR_SPECIFIED; + if (!strcmp(s, "エミッタ方向")) + return S3DE_DIR_EMITTER; + if (!strcmp(s, "エミッタ逆方向")) + return S3DE_DIR_EMITTER_REVERSE; + if (!strcmp(s, "エミッタ座標系")) + return S3DE_DIR_EMITTER_COORD; + if (!strcmp(s, "任意平面")) + return S3DE_DIR_ARBITRARY_PLANE; + if (!strcmp(s, "生成方向")) + return S3DE_DIR_SPAWN; + WARNING("unknown direction type \"%s\"", s); + return (enum s3de_direction_type)-1; +} + +static enum s3de_posture_type parse_posture_type(const char *s) +{ + if (!strcmp(s, "なし")) + return S3DE_POSE_NONE; + if (!strcmp(s, "カメラ")) + return S3DE_POSE_CAMERA; + if (!strcmp(s, "移動方向")) + return S3DE_POSE_MOVE_DIR; + if (!strcmp(s, "移動&飛行方向")) + return S3DE_POSE_MOVE_AND_FLY; + if (!strcmp(s, "飛行&エミッタ移動方向")) + return S3DE_POSE_FLY_AND_EMITTER_MOVE; + WARNING("unknown posture type \"%s\"", s); + return (enum s3de_posture_type)-1; +} + +static enum s3de_interp_type parse_interp_type(const char *s) +{ + if (!strcmp(s, "なし")) + return S3DE_INTERP_NONE; + if (!strcmp(s, "線形")) + return S3DE_INTERP_LINEAR; + if (!strcmp(s, "球面線形")) + return S3DE_INTERP_SLERP; + WARNING("unknown interp type \"%s\"", s); + return (enum s3de_interp_type)-1; +} + +#define MATCH(fmt, ...) (len = 0, sscanf(text, fmt " %n", ##__VA_ARGS__, &len), text += len, len) +#define STRING_BUF_SIZE 256 +#define KEYWORD " %255[^, \t\r\n}]" +#define QUOTED_STRING "\"%255[^\"]\"" + +static char *parse_quoted_string(char **ptext) +{ + char *text = *ptext; + + while (isspace((unsigned char)*text)) text++; + if (*text++ != '"') { + parse_error("quoted string expected.", text); + return NULL; + } + char *p = strchr(text, '"'); + if (!p) { + parse_error("unfinished string.", text); + return NULL; + } + *p = '\0'; + *ptext = p + 1; + return utf2sjis(text, 0); +} + +// range := float [',' float] +static bool parse_range(char **ptext, struct s3de_range *r) +{ + char *text = *ptext; + int len; + float a, b; + if (!MATCH("%f", &a)) { + parse_error("float expected.", text); + return false; + } + r->base = a; + r->random = 0.0f; + if (MATCH(", %f", &b)) + r->random = b; + *ptext = text; + return true; +} + +static bool parse_two_floats(char **ptext, float out[2]) +{ + char *text = *ptext; + int len; + if (!MATCH("%f , %f", &out[0], &out[1])) { + parse_error("two floats expected.", text); + return false; + } + *ptext = text; + return true; +} + +static bool parse_spin_angle(char **ptext, struct s3de_range *start, struct s3de_range *end) +{ + float v[2]; + if (!parse_two_floats(ptext, v)) + return false; + start->base = v[0]; + start->random = 0.0f; + end->base = v[1]; + end->random = 0.0f; + return true; +} + +static bool parse_vec3(char **ptext, vec3 out) +{ + char *text = *ptext; + int len; + if (!MATCH("%f , %f , %f", &out[0], &out[1], &out[2])) { + parse_error("three floats expected.", text); + return false; + } + *ptext = text; + return true; +} + +static int *parse_int_list(char **ptext, int *nr) +{ + char *text = *ptext; + int len, n; + if (!MATCH("%d", &n)) { + parse_error("int expected.", text); + return NULL; + } + int cap = 16; + int *v = xmalloc(cap * sizeof(int)); + v[0] = n; + *nr = 1; + while (MATCH(", %d", &n)) { + if (*nr == cap) { + cap *= 2; + v = xrealloc(v, cap * sizeof(int)); + } + v[(*nr)++] = n; + } + *ptext = text; + return v; +} + +// Parses a comma-separated list of quoted strings, storing the first non-empty +// one in *out and skipping the rest. Rance 9 only uses the first name. +static bool parse_texture_name(char **ptext, char **out) +{ + char *text = *ptext; + int len; + char *result = NULL; + do { + char *s = parse_quoted_string(&text); + if (!s) { + free(result); + return false; + } + if (result || s[0] == '\0') + free(s); + else + result = s; + } while (MATCH(", ")); + *ptext = text; + *out = result; + return true; +} + +// Parses a list of scalar entries of the form "{ frame, value, interp }, ...". +static bool parse_scalar_entries(char **ptext, struct s3de_scalar_entry **out, int *nr_out) +{ + char *text = *ptext; + int len; + int cap = 8, nr = 0; + char buf[STRING_BUF_SIZE]; + struct s3de_scalar_entry *v = xmalloc(cap * sizeof(*v)); + for (;;) { + if (!MATCH("{ %d , %f , " KEYWORD, &v[nr].frame, &v[nr].value, buf)) { + parse_error("scalar list entry expected.", text); + goto err; + } + int interp = parse_interp_type(buf); + if (interp < 0) goto err; + v[nr].interp = interp; + if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; } + nr++; + if (nr == cap) { + cap *= 2; + v = xrealloc(v, cap * sizeof(*v)); + } + if (!MATCH(",")) break; + } + *out = v; + *nr_out = nr; + *ptext = text; + return true; +err: + free(v); + return false; +} + +// Parses a list of vec3 entries of the form "{ frame, x, y, z, interp }, ...". +static bool parse_vec3_entries(char **ptext, struct s3de_vec3_entry **out, int *nr_out) +{ + char *text = *ptext; + int len; + int cap = 8, nr = 0; + char buf[STRING_BUF_SIZE]; + struct s3de_vec3_entry *v = xmalloc(cap * sizeof(*v)); + for (;;) { + if (!MATCH("{ %d , %f , %f , %f , " KEYWORD, + &v[nr].frame, &v[nr].v[0], &v[nr].v[1], &v[nr].v[2], buf)) { + parse_error("vec3 list entry expected.", text); + goto err; + } + int interp = parse_interp_type(buf); + if (interp < 0) goto err; + v[nr].interp = interp; + if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; } + nr++; + if (nr == cap) { + cap *= 2; + v = xrealloc(v, cap * sizeof(*v)); + } + if (!MATCH(",")) break; + } + *out = v; + *nr_out = nr; + *ptext = text; + return true; +err: + free(v); + return false; +} + +// Parses a list of position entries of the form +// "{ frame, x, y, z, interp, [spawn_radius, spawn_angle_deg] }, ...". +static bool parse_position_entries(char **ptext, struct s3de_position_entry **out, int *nr_out) +{ + char *text = *ptext; + int len; + int cap = 8, nr = 0; + char buf[STRING_BUF_SIZE]; + struct s3de_position_entry *v = xmalloc(cap * sizeof(*v)); + for (;;) { + memset(&v[nr], 0, sizeof(v[nr])); + if (!MATCH("{ %d , %f , %f , %f , " KEYWORD, + &v[nr].frame, &v[nr].pos[0], &v[nr].pos[1], &v[nr].pos[2], buf)) { + parse_error("position list entry expected.", text); + goto err; + } + // Convert from left-handed coords to right-handed coords. + v[nr].pos[2] = -v[nr].pos[2]; + int interp = parse_interp_type(buf); + if (interp < 0) goto err; + v[nr].interp = interp; + (void)MATCH(", %f , %f", &v[nr].spawn_radius, &v[nr].spawn_angle_deg); + if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; } + nr++; + if (nr == cap) { + cap *= 2; + v = xrealloc(v, cap * sizeof(*v)); + } + if (!MATCH(",")) break; + } + *out = v; + *nr_out = nr; + *ptext = text; + return true; +err: + free(v); + return false; +} + +static struct s3de *parse_3de(char *text) +{ + struct s3de *s = xcalloc(1, sizeof(struct s3de)); + s->loop_start_frame = -1; + s->loop_end_frame = -1; + + struct s3de_object *current = NULL; + + for (;;) { + // Skip whitespace and // comments. + for (;;) { + while (isspace((unsigned char)*text)) text++; + if (text[0] == '/' && text[1] == '/') { + char *nl = strchr(text, '\n'); + text = nl ? nl + 1 : text + strlen(text); + } else { + break; + } + } + + int len; + int n; + float f; + char buf[STRING_BUF_SIZE]; + + if (!current) { + if (!*text) return s; + + if (MATCH("ループ開始フレーム = %d", &n)) { + s->loop_start_frame = n; + } else if (MATCH("ループ終了フレーム = %d", &n)) { + s->loop_end_frame = n; + } else if (MATCH("オブジェクト " QUOTED_STRING " = {", buf)) { + s->objects = xrealloc_array(s->objects, s->nr_objects, + s->nr_objects + 1, sizeof(struct s3de_object)); + current = &s->objects[s->nr_objects++]; + current->name = utf2sjis(buf, 0); + current->texture_anime_frame = 1.0f; + } else { + parse_error("unknown toplevel token.", text); + goto err; + } + continue; + } + + if (!*text) { + WARNING("unexpected EOF inside object"); + goto err; + } + + if (MATCH("}")) { + current = NULL; + } else if (MATCH("種類 = " KEYWORD, buf)) { + int v = parse_object_type(buf); + if (v < 0) goto err; + current->type = v; + } else if (MATCH("移動タイプ = " KEYWORD, buf)) { + int v = parse_move_type(buf); + if (v < 0) goto err; + current->movement_type = v; + } else if (MATCH("生成位置タイプ = " KEYWORD, buf)) { + int v = parse_spawn_position_type(buf); + if (v < 0) goto err; + current->spawn_position_type = v; + } else if (MATCH("生成距離 = %f", &f)) { + current->spawn_distance = f; + } else if (MATCH("個数 = %d", &n)) { + current->particle_count = n; + } else if (MATCH("テクスチャ = ")) { + if (!parse_texture_name(&text, ¤t->texture)) goto err; + } else if (MATCH("ブレンドタイプ = " KEYWORD, buf)) { + int v = parse_blend_type(buf); + if (v < 0) goto err; + current->blend_type = v; + } else if (MATCH("テクスチャアニメフレーム = %f", &f)) { + current->texture_anime_frame = f; + } else if (MATCH("ポリゴン = ")) { + current->polygon_name = parse_quoted_string(&text); + if (!current->polygon_name) goto err; + } else if (MATCH("フレーム参照 = " KEYWORD, buf)) { + // Every emitter in Rance 9 uses "エフェクト". + if (strcmp(buf, "エフェクト") != 0) + WARNING("unhandled frame_reference \"%s\"", buf); + } else if (MATCH("エミッタ連動タイプ = %d", &n)) { + if (n < 0 || n > S3DE_LINK_MAX) { + WARNING("unknown emitter_link_type %d", n); + n = S3DE_LINK_NONE; + } + current->emitter_link_type = n; + } else if (MATCH("ソフトフォグエッジ = %d", &n)) { + current->soft_fog_edge = (n != 0); + } else if (MATCH("姿勢タイプ = " KEYWORD, buf)) { + int v = parse_posture_type(buf); + if (v < 0) goto err; + current->posture_type = v; + } else if (MATCH("ターゲット名 = ")) { + // Unused. + char *s = parse_quoted_string(&text); + if (!s) goto err; + free(s); + } else if (MATCH("方向タイプ = " KEYWORD, buf)) { + int v = parse_direction_type(buf); + if (v < 0) goto err; + current->direction_type = v; + } else if (MATCH("方向 = ")) { + if (!parse_vec3(&text, current->direction)) + goto err; + current->direction[2] = -current->direction[2]; + } else if (MATCH("方向角度 = %f", &f)) { + current->direction_angle = f; + } + else if (MATCH("開始サイズ = ")) { if (!parse_range(&text, ¤t->start_size)) goto err; } + else if (MATCH("終了サイズ = ")) { if (!parse_range(&text, ¤t->end_size)) goto err; } + else if (MATCH("開始Xサイズ = ")) { if (!parse_range(&text, ¤t->start_x_size)) goto err; } + else if (MATCH("終了Xサイズ = ")) { if (!parse_range(&text, ¤t->end_x_size)) goto err; } + else if (MATCH("開始Yサイズ = ")) { if (!parse_range(&text, ¤t->start_y_size)) goto err; } + else if (MATCH("終了Yサイズ = ")) { if (!parse_range(&text, ¤t->end_y_size)) goto err; } + else if (MATCH("オフセットX = ")) { if (!parse_range(&text, ¤t->offset_x)) goto err; } + else if (MATCH("オフセットY = ")) { if (!parse_range(&text, ¤t->offset_y)) goto err; } + else if (MATCH("オフセットZ = ")) { + if (!parse_range(&text, ¤t->offset_z)) + goto err; + current->offset_z.base = -current->offset_z.base; + } + else if (MATCH("速度 = ")) { if (!parse_range(&text, ¤t->speed)) goto err; } + else if (MATCH("加速度 = ")) { if (!parse_range(&text, ¤t->acceleration)) goto err; } + else if (MATCH("移動距離 = ")) { if (!parse_range(&text, ¤t->movement_distance)) goto err; } + else if (MATCH("移動カーブ = ")) { if (!parse_range(&text, ¤t->movement_curve)) goto err; } + else if (MATCH("自由落下 = %d", &n)) { current->free_fall = (n != 0); } + else if (MATCH("質量 = %f", &f)) { current->mass = f; } + else if (MATCH("空気抵抗係数 = %f", &f)) { current->air_resistance = f; } + else if (MATCH("開始X回転角度 = ")) { if (!parse_range(&text, ¤t->start_x_rotation)) goto err; } + else if (MATCH("終了X回転角度 = ")) { if (!parse_range(&text, ¤t->end_x_rotation)) goto err; } + else if (MATCH("開始Y回転角度 = ")) { if (!parse_range(&text, ¤t->start_y_rotation)) goto err; } + else if (MATCH("終了Y回転角度 = ")) { if (!parse_range(&text, ¤t->end_y_rotation)) goto err; } + else if (MATCH("開始Z回転角度 = ")) { if (!parse_range(&text, ¤t->start_z_rotation)) goto err; } + else if (MATCH("終了Z回転角度 = ")) { if (!parse_range(&text, ¤t->end_z_rotation)) goto err; } + else if (MATCH("X軸自転角度 = ")) { if (!parse_spin_angle(&text, ¤t->start_x_rotation, ¤t->end_x_rotation)) goto err; } + else if (MATCH("Y軸自転角度 = ")) { if (!parse_spin_angle(&text, ¤t->start_y_rotation, ¤t->end_y_rotation)) goto err; } + else if (MATCH("Z軸自転角度 = ")) { if (!parse_spin_angle(&text, ¤t->start_z_rotation, ¤t->end_z_rotation)) goto err; } + else if (MATCH("X軸公転角度 = ")) { if (!parse_two_floats(&text, current->x_revolution_angle)) goto err; } + else if (MATCH("Y軸公転角度 = ")) { if (!parse_two_floats(&text, current->y_revolution_angle)) goto err; } + else if (MATCH("Z軸公転角度 = ")) { if (!parse_two_floats(&text, current->z_revolution_angle)) goto err; } + else if (MATCH("X軸公転距離 = ")) { if (!parse_two_floats(&text, current->x_revolution_distance)) goto err; } + else if (MATCH("Y軸公転距離 = ")) { if (!parse_two_floats(&text, current->y_revolution_distance)) goto err; } + else if (MATCH("Z軸公転距離 = ")) { if (!parse_two_floats(&text, current->z_revolution_distance)) goto err; } + else if (MATCH("カーブX距離 = %f", &f)) { /* unused */ } + else if (MATCH("カーブY距離 = %f", &f)) { /* unused */ } + else if (MATCH("カーブZ距離 = %f", &f)) { /* unused */ } + else if (MATCH("子フレーム = %d", &n)) { current->child_frame = n; } + else if (MATCH("子傾き = %f", &f)) { /* unused */ } + else if (MATCH("子傾き2 = %f", &f)) { /* unused */ } + else if (MATCH("子移動方向タイプ = %d", &n)) { /* unused */ } + else if (MATCH("アップベクトルタイプ = %d", &n)) { /* unused */ } + else if (MATCH("停止フレーム = %d", &n)) { /* unused */ } + else if (MATCH("アルファフェードインフレーム = ")) { if (!parse_range(&text, ¤t->alpha_fade_in_frame)) goto err; } + else if (MATCH("アルファフェードアウトフレーム = ")) { if (!parse_range(&text, ¤t->alpha_fade_out_frame)) goto err; } + else if (MATCH("ダメージ = ")) { + current->damages = parse_int_list(&text, ¤t->nr_damages); + if (!current->damages) + goto err; + } + else if (MATCH("位置リスト = ")) { + if (!parse_position_entries(&text, ¤t->position_list, ¤t->nr_position_entries)) + goto err; + } + else if (MATCH("サイズリスト = ")) { + if (!parse_scalar_entries(&text, ¤t->size_list, ¤t->nr_size_entries)) + goto err; + } + else if (MATCH("Xサイズリスト = ")) { + if (!parse_scalar_entries(&text, ¤t->x_size_list, ¤t->nr_x_size_entries)) + goto err; + } + else if (MATCH("Yサイズリスト = ")) { + if (!parse_scalar_entries(&text, ¤t->y_size_list, ¤t->nr_y_size_entries)) + goto err; + } + else if (MATCH("アルファリスト = ")) { + if (!parse_scalar_entries(&text, ¤t->alpha_list, ¤t->nr_alpha_entries)) + goto err; + } + else if (MATCH("回転リスト = ")) { + if (!parse_vec3_entries(&text, ¤t->rotation_list, ¤t->nr_rotation_entries)) + goto err; + } + else if (MATCH("乗算カラーリスト = ")) { + if (!parse_vec3_entries(&text, ¤t->multiply_color_list, ¤t->nr_multiply_color_entries)) + goto err; + } + else if (MATCH("加算カラーリスト = ")) { + if (!parse_vec3_entries(&text, ¤t->additive_color_list, ¤t->nr_additive_color_entries)) + goto err; + } + else { + parse_error("unknown object field.", text); + goto err; + } + } +err: + s3de_free(s); + return NULL; +} + +struct s3de *s3de_load(struct archive *aar, const char *path) +{ + const char *basename = strrchr(path, '\\'); + basename = basename ? basename + 1 : path; + + struct archive_data *dfile = RE_get_aar_entry(aar, path, basename, ".3de"); + if (!dfile) { + WARNING("%s\\%s.3de: not found", path, basename); + return NULL; + } + + char *sjis_text = xmalloc(dfile->size + 1); + memcpy(sjis_text, dfile->data, dfile->size); + sjis_text[dfile->size] = '\0'; + + char *text = sjis2utf(sjis_text, dfile->size); + free(sjis_text); + struct s3de *s = parse_3de(text); + free(text); + if (!s) { + WARNING("could not parse %s", dfile->name); + archive_free_data(dfile); + return NULL; + } + archive_free_data(dfile); + + s->path = strdup(path); + + return s; +} + +void s3de_free(struct s3de *s) +{ + if (!s) return; + for (int i = 0; i < s->nr_objects; i++) { + struct s3de_object *o = &s->objects[i]; + free(o->name); + free(o->texture); + free(o->polygon_name); + free(o->damages); + free(o->position_list); + free(o->size_list); + free(o->x_size_list); + free(o->y_size_list); + free(o->alpha_list); + free(o->rotation_list); + free(o->multiply_color_list); + free(o->additive_color_list); + } + free(s->objects); + free(s->path); + free(s); +} + +static void interp_vec3(struct s3de_vec3_entry *list, int n, float frame, vec3 dest, vec3 def) +{ + if (n == 0) { + glm_vec3_copy(def, dest); + return; + } + if (frame <= list[0].frame) { + glm_vec3_copy(list[0].v, dest); + return; + } + if (frame >= list[n - 1].frame) { + glm_vec3_copy(list[n - 1].v, dest); + return; + } + int k = 0; + while (k + 1 < n && list[k + 1].frame <= frame) + k++; + struct s3de_vec3_entry *a = &list[k]; + struct s3de_vec3_entry *b = &list[k + 1]; + if (a->interp == S3DE_INTERP_NONE) { + glm_vec3_copy(a->v, dest); + return; + } + float t = (frame - a->frame) / (float)(b->frame - a->frame); + glm_vec3_lerp(a->v, b->v, t, dest); +} + +static void interp_pos(struct s3de_position_entry *list, int n, float frame, vec3 dest) +{ + if (n == 0) { + glm_vec3_zero(dest); + return; + } + if (frame <= list[0].frame) { + glm_vec3_copy(list[0].pos, dest); + return; + } + if (frame >= list[n - 1].frame) { + glm_vec3_copy(list[n - 1].pos, dest); + return; + } + int k = 0; + while (k + 1 < n && list[k + 1].frame <= frame) + k++; + struct s3de_position_entry *a = &list[k]; + struct s3de_position_entry *b = &list[k + 1]; + if (a->interp == S3DE_INTERP_NONE) { + glm_vec3_copy(a->pos, dest); + return; + } + float t = (frame - a->frame) / (float)(b->frame - a->frame); + glm_vec3_lerp(a->pos, b->pos, t, dest); +} + +struct s3de_effect *s3de_effect_create(struct s3de *s, struct archive *aar) +{ + struct s3de_effect *eff = xcalloc(1, sizeof(struct s3de_effect)); + eff->s3de = s; + eff->wav_channel = -1; + + const char *basename = strrchr(s->path, '\\'); + basename = basename ? basename + 1 : s->path; + struct archive_data *dfile = RE_get_aar_entry(aar, s->path, basename, ".wav"); + if (dfile) { + int ch = wav_get_unused_channel(); + if (wav_prepare_from_archive_data(ch, dfile)) { + eff->wav_channel = ch; + } else { + WARNING("wav_prepare_from_archive_data failed for %s\\%s.wav", s->path, basename); + } + } + return eff; +} + +void s3de_effect_free(struct s3de_effect *eff) +{ + if (!eff) + return; + if (eff->wav_channel >= 0) { + wav_stop(eff->wav_channel); + wav_unprepare(eff->wav_channel); + } + free(eff); +} + +// Pick the camera object whose position_list frame range contains `frame`. +// If `frame` is past every range, pick the camera with the latest last_frame. +// If `frame` precedes every range, return -1. +static int select_active_camera_obj(struct s3de_effect *eff, float frame) +{ + int best = -1; + float best_last = -INFINITY, best_first = -INFINITY; + for (int i = 0; i < eff->s3de->nr_objects; i++) { + struct s3de_object *obj = &eff->s3de->objects[i]; + if (obj->type != S3DE_OBJ_CAMERA || obj->nr_position_entries == 0) + continue; + float first = obj->position_list[0].frame; + float last = obj->position_list[obj->nr_position_entries - 1].frame; + if (first > frame) + continue; + if (frame < last) + return i; + if (last > best_last || (last == best_last && first > best_first)) { + best = i; + best_last = last; + best_first = first; + } + } + return best; +} + +static void update_sound(struct s3de_effect *eff, struct motion *m) +{ + if (eff->wav_channel < 0) + return; + bool stopped = (m->state == RE_MOTION_STATE_STOP); + float frame = m->current_frame; + if (eff->sound_started && (stopped || frame + 0.5f < eff->last_frame)) { + wav_stop(eff->wav_channel); + eff->sound_started = false; + } + if (!eff->sound_started && !stopped) { + int ms = (int)(frame * 1000.0f / 30.0f); + if (ms > 0) + wav_seek(eff->wav_channel, ms); + wav_play(eff->wav_channel); + eff->sound_started = true; + } + eff->last_frame = frame; +} + +static void update_camera_override(struct RE_instance *inst, struct s3de_effect *eff, float frame) +{ + int cam_idx = select_active_camera_obj(eff, frame); + if (cam_idx < 0 || !inst->plugin) + return; + + struct s3de_object *obj = &eff->s3de->objects[cam_idx]; + vec3 emitter_pos, emitter_rotation_deg; + interp_pos(obj->position_list, obj->nr_position_entries, frame, emitter_pos); + interp_vec3(obj->rotation_list, obj->nr_rotation_entries, frame, + emitter_rotation_deg, GLM_VEC3_ZERO); + + if (inst->local_transform_needs_update) + RE_instance_update_local_transform(inst); + vec4 local_pos = { emitter_pos[0], emitter_pos[1], emitter_pos[2], 1.0f }; + vec4 world_pos; + glm_mat4_mulv(inst->local_transform, local_pos, world_pos); + struct RE_camera *cam = &inst->plugin->camera; + glm_vec3_copy(world_pos, cam->override_pos); + // XXX: Compose the instance and camera-object rotations by summing Euler + // angles directly. This is not equivalent to true rotation composition in + // general, but it is exact here: in Rance 9 non-camera instances are only + // ever rotated about yaw. Since yaw is the outermost axis in the renderer's + // YXZ convention, the two yaw rotations combine exactly by addition. + cam->override_pitch = inst->pitch + emitter_rotation_deg[0]; + cam->override_yaw = inst->yaw + emitter_rotation_deg[1]; + cam->override_roll = -(inst->roll + emitter_rotation_deg[2]); + cam->override_active = true; +} + +void s3de_effect_update(struct RE_instance *inst) +{ + struct s3de_effect *eff = inst->s3de_effect; + if (!eff || !inst->motion) + return; + update_sound(eff, inst->motion); + float frame = inst->motion->current_frame; + update_camera_override(inst, eff, frame); +} + +void s3de_calc_frame_range(struct s3de *s, struct motion *motion) +{ + int begin = s->loop_start_frame >= 0 ? s->loop_start_frame : 0; + int end = s->loop_end_frame >= 0 ? s->loop_end_frame : begin; + motion->frame_begin = motion->loop_frame_begin = begin; + motion->frame_end = motion->loop_frame_end = end; +} diff --git a/src/meson.build b/src/meson.build index 0f57f4c..13294e7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -45,6 +45,7 @@ xsystem4 = [version_h, '3d/particle.c', '3d/reign.c', '3d/renderer.c', + '3d/s3de.c', 'dungeon/dgn.c', 'dungeon/dtx.c',