[Decompilation] [th01] Boss defeat sequence: SinGyoku version + route selection

Part of P0167, funded by Ember2528.
This commit is contained in:
nmlgc 2021-11-05 20:45:34 +01:00
parent d5ec2220ec
commit 021cbc8e9d
10 changed files with 295 additions and 381 deletions

6
pc98.h
View File

@ -97,6 +97,12 @@ typedef int8_t uint4_t;
static ComponentType max() {
return (Range - 1);
}
void set(ComponentType r, ComponentType g, ComponentType b) {
this->c.r = r;
this->c.g = g;
this->c.b = b;
}
};
template <class RGBType> struct Palette {

View File

@ -1,10 +0,0 @@
// Performs a very slow, unoptimized, 2× nearest-neighbor scale of the
// rectangle from
// (⌊left_1/8⌋*8, top_1) - (⌊left_1/8⌋*8 + ⌊w_1/16⌋*16, top_1 + h_1)
// on plane #1 to
// (⌊left_0/8⌋*8, top_0) - (⌊left_1/8⌋*8 + ⌊w_1/16⌋*32, top_1 + h_1*2)
// on plane #0.
void graph_2xscale_byterect_1_to_0_slow(
screen_x_t left_0, vram_y_t top_0,
screen_x_t left_1, vram_y_t top_1, pixel_t w_1, pixel_t h_1
);

View File

@ -0,0 +1,31 @@
// Performs a very slow, unoptimized, 2× nearest-neighbor scale of the
// rectangle from
// (⌊left_1/8⌋*8, top_1) - (⌊left_1/8⌋*8 + ⌊w_1/16⌋*16, top_1 + h_1)
// on plane #1 to
// (⌊left_0/8⌋*8, top_0) - (⌊left_1/8⌋*8 + ⌊w_1/16⌋*32, top_1 + h_1*2)
// on plane #0.
void graph_2xscale_byterect_1_to_0_slow(
screen_x_t left_0, vram_y_t top_0,
screen_x_t left_1, vram_y_t top_1, pixel_t w_1, pixel_t h_1
);
// Convenience function for blitting and scaling rows of glyphs
// ------------------------------------------------------------
// (Which are the only things that this 2× scale is performed on anyway.)
// Blits [str] to the given glyph [row] with the given color and effects, for
// a later 2× scale of the
inline void graph_glyphrow_put(int row, int col_and_fx, const char *str) {
graph_putsa_fx(0, (GLYPH_H * row), (FX_CLEAR_BG | col_and_fx), str);
}
// Performs a 2× nearest-neighbor scale of the first [w_1x] pixels from the
// given glyph [row] on VRAM page 1 to (⌊left_0/8⌋*8, top_0) on VRAM page 0.
inline void graph_glyphrow_2xscale_1_to_0(
screen_x_t left_0, vram_y_t top_0, int row, pixel_t w_1x
) {
graph_2xscale_byterect_1_to_0_slow(
left_0, top_0, 0, (GLYPH_H * row), w_1x, GLYPH_H
);
}
// ------------------------------------------------------------

View File

@ -55,6 +55,12 @@ void z_palette_white_out(void);
z_palette_set_all_show(z_Palettes); \
}
// Sets all colors in both the hardware palette and z_Palettes to #FFF.
#define z_palette_set_white(tmp_col, tmp_comp) { \
palette_set_grayscale(z_Palettes, RGB4::max(), tmp_col, tmp_comp); \
z_palette_set_all_show(z_Palettes); \
}
// Performs a single black-out step.
#define z_palette_black_out_step(tmp_col, tmp_comp) \
z_Palettes_set_func_and_show(tmp_col, tmp_comp, { \
@ -68,6 +74,14 @@ void z_palette_white_out(void);
(z_Palettes.colors[tmp_col].v[tmp_comp] > 0) ? -1 : 0; \
})
// Performs a single white-out step.
#define z_palette_white_out_step(tmp_col, tmp_comp) \
z_Palettes_set_func_and_show(tmp_col, tmp_comp, { \
if(z_Palettes.colors[tmp_col].v[tmp_comp] < 0xF) { \
z_Palettes.colors[tmp_col].v[tmp_comp]++; \
} \
})
// Performs a single ramping step from z_Palettes to [target_pal]. Every color
// in z_Palettes is assumed to not be brighter than the corresponding color in
// [target_pal].

View File

@ -1,7 +1,7 @@
#include "th01/math/clamp.hpp"
#include "th01/hardware/egc.h"
#include "th01/hardware/frmdelay.h"
#include "th01/hardware/grp2xscs.h"
#include "th01/hardware/grp2xscs.hpp"
#include "th01/v_colors.hpp"
#include "th01/score.h"

View File

@ -49,6 +49,11 @@ extern Palette4 boss_post_defeat_palette;
// Individual bosses
// -----------------
static const pixel_t SINGYOKU_W = 96;
// Actually required publically, as singyoku_defeat_animate_and_select_route()
// is part of the regular boss defeat translation unit.
static const pixel_t SINGYOKU_H = 96;
// Makai
void elis_load(void);
void elis_free(void);

View File

@ -1,10 +1,32 @@
extern "C" {
#include <stddef.h>
#include "platform.h"
#include "pc98.h"
#include "planar.h"
#include "th01/common.h"
#include "th01/resident.hpp"
#include "th01/v_colors.hpp"
#include "th01/main/playfld.hpp"
#include "th01/formats/pf.hpp"
#include "th01/formats/ptn.hpp"
#include "th01/formats/stagedat.hpp"
#include "th01/math/area.hpp"
#include "th01/hardware/egc.h"
#include "th01/hardware/frmdelay.h"
#include "th01/hardware/graph.h"
#include "th01/hardware/input.hpp"
#include "th01/hardware/grp2xscs.hpp"
#include "th01/hardware/palette.h"
#include "th01/hardware/scrollup.hpp"
#include "th01/snd/mdrv2.h"
#include "th01/main/boss/entity_a.hpp"
#include "th01/main/player/orb.hpp"
#include "th01/main/player/player.hpp"
#include "th01/shiftjis/routesel.hpp"
}
#include "th01/main/boss/boss.hpp"
#include "th01/main/boss/defeat.hpp"
#include "th01/main/stage/stageobj.hpp"
void grcg_whiteline(screen_y_t y)
{
@ -16,3 +38,194 @@ void grcg_whiteline(screen_y_t y)
}
grcg_off();
}
struct defeat_anim_t {
int frame;
int components_done;
screen_y_t bottom;
screen_y_t top;
};
#define defeat_animate( \
start_y, line_distance, whiteout_interval, whitein_interval \
) \
int col; \
int comp; \
screen_y_t top; \
screen_y_t bottom; \
int components_done; \
int frame = 0; \
\
z_vsync_wait_and_scrollup(0); \
\
top = start_y; \
bottom = start_y; \
\
while(1) { \
if(top >= 0) { \
grcg_whiteline(top); \
} \
if(bottom <= (RES_Y - 1)) { \
grcg_whiteline(bottom); \
} \
top -= line_distance; \
bottom += line_distance; \
frame++; \
if((top < 0) && (bottom > (RES_Y - 1))) { \
break; \
} \
if((frame % whiteout_interval) == 0) { \
z_palette_white_out_step(col, comp); \
} \
frame_delay(1); \
} \
\
/* Aww? No ramping loop? */ \
frame = 0; \
z_palette_set_white(col, comp); \
\
/* Unblit all white lines and return to the regular stage background */ \
graph_accesspage_func(1); \
graph_copy_accessed_page_to_other(); \
graph_accesspage_func(0); \
\
/* Reimu might not have been standing still, after all. */ \
ptn_put_8(player_left, player_top, PTN_MIKO_L); \
\
/* Fade to [boss_post_defeat_palette] */ \
while(1) { \
frame++; \
components_done = 0; \
if((frame % whitein_interval) == 0) { \
z_Palettes_set_func_and_show(col, comp, { \
if( \
z_Palettes[col].v[comp] > \
boss_post_defeat_palette[col].v[comp] \
) { \
z_Palettes[col].v[comp]--; \
} else { \
components_done++; \
} \
}); \
} \
if(components_done >= (COLOR_COUNT * COMPONENT_COUNT)) { \
break; \
} \
frame_delay(1); \
}
void singyoku_defeat_animate_and_select_route(void)
{
defeat_animate(
(boss_entities[0].cur_top + (SINGYOKU_H / 2)), 1, 15, 15
);
palette_foreach(col, comp, {
if(z_Palettes[col].v[comp] > 0) {
// Relies on the clamping behavior of z_palette_set_show().
// (The >0 check wouldn't even have been necessary.)
z_Palettes[col].v[comp] = (z_Palettes[col].v[comp] - 0x4);
}
});
z_Palettes[V_WHITE].set(RGB4::max(), RGB4::max(), RGB4::max());
z_palette_set_all_show(z_Palettes);
// Route selection
enum {
MAKAI_TOP = 250,
JIGOKU_TOP = 300,
CURSOR_LEFT = 128,
};
struct {
int v; // route_t
void render(int col_sel, int col_other) {
z_palette_set_show(col_sel, RGB4::max(), RGB4::max(), RGB4::max());
z_palette_set_show(col_other, 0x9, 0x9, 0x9);
}
} route_sel;
#undef ROUTE_SEL_1
#undef ROUTE_SEL_2
#undef ROUTE_SEL_3
#undef ROUTE_SEL_4
#undef ROUTE_SEL_5
extern const char ROUTE_SEL_1[];
extern const char ROUTE_SEL_2[];
extern const char ROUTE_SEL_3[];
extern const char ROUTE_SEL_4[];
extern const char ROUTE_SEL_5[];
graph_accesspage_func(1);
z_graph_clear(); // redundant
graph_glyphrow_put(0, V_WHITE, ROUTE_SEL_1);
graph_glyphrow_put(2, V_WHITE, ROUTE_SEL_2);
graph_glyphrow_put(4, V_WHITE, ROUTE_SEL_3);
graph_glyphrow_put(6, COL_MAKAI, ROUTE_SEL_4);
graph_glyphrow_put(8, COL_JIGOKU, ROUTE_SEL_5);
graph_accesspage_func(0);
route_sel.render(COL_MAKAI, COL_JIGOKU);
// Re-#define the literals...
#include "th01/shiftjis/routesel.hpp"
graph_glyphrow_2xscale_1_to_0(64, 64, 0, shiftjis_w(ROUTE_SEL_1));
graph_glyphrow_2xscale_1_to_0(32, 96, 2, shiftjis_w(ROUTE_SEL_2));
graph_glyphrow_2xscale_1_to_0(32, 180, 4, shiftjis_w(ROUTE_SEL_3));
graph_glyphrow_2xscale_1_to_0(256, MAKAI_TOP, 6, shiftjis_w(ROUTE_SEL_4));
graph_glyphrow_2xscale_1_to_0(256, JIGOKU_TOP, 8, shiftjis_w(ROUTE_SEL_5));
graph_copy_accessed_page_to_other();
ptn_put_8(CURSOR_LEFT, MAKAI_TOP, PTN_ORB);
route_sel.v = ROUTE_MAKAI;
input_shot = false;
input_ok = false;
input_reset_sense();
bool16 holding_up = false;
bool16 holding_down = false;
frame_delay(50);
while(1) {
input_sense(false);
if((input_up && !holding_up) || (input_down && !holding_down)) {
if(route_sel.v == ROUTE_MAKAI) {
egc_copy_rect_1_to_0_16(CURSOR_LEFT, MAKAI_TOP, ORB_W, ORB_H);
ptn_put_8(CURSOR_LEFT, JIGOKU_TOP, PTN_ORB);
route_sel.render(COL_JIGOKU, COL_MAKAI);
route_sel.v = ROUTE_JIGOKU;
} else {
egc_copy_rect_1_to_0_16(CURSOR_LEFT, JIGOKU_TOP, ORB_W, ORB_H);
ptn_put_8(CURSOR_LEFT, MAKAI_TOP, PTN_ORB);
route_sel.render(COL_MAKAI, COL_JIGOKU);
route_sel.v = ROUTE_MAKAI;
}
}
if(!input_up) {
holding_up = false;
}
if(!input_down) {
holding_down = false;
}
if(input_up) {
holding_up = true;
}
if(input_down) {
holding_down = true;
}
if((input_shot == true) || (input_ok == true)) {
mdrv2_se_play(4);
break;
}
frame_delay(5);
}
if(route_sel.v == ROUTE_MAKAI) {
scene_init_and_load(1);
} else {
scene_init_and_load(2);
}
route = static_cast<route_t>(route_sel.v);
stage_cleared = true;
done = true;
}

10
th01/main/boss/defeat.hpp Normal file
View File

@ -0,0 +1,10 @@
enum route_col_t {
COL_MAKAI = 1,
COL_JIGOKU = 3,
};
// Shows a special SinGyoku version of the regular boss defeat animation,
// followed by the Makai/Jigoku route selection and marking the current stage
// as cleared. Repurposes the VRAM colors [COL_MAKAI] and [COL_JIGOKU] for the
// route selection cursor.
void singyoku_defeat_animate_and_select_route(void);

View File

@ -0,0 +1,5 @@
#define ROUTE_SEL_1 "  "
#define ROUTE_SEL_2 ""
#define ROUTE_SEL_3 "  "
#define ROUTE_SEL_4 "魔界へ"
#define ROUTE_SEL_5 "地獄へ"

View File

@ -3637,369 +3637,7 @@ main_17__TEXT segment byte public 'CODE' use16
assume es:nothing, ss:nothing, ds:_DATA, fs:nothing, gs:nothing
extern @grcg_whiteline$qi:proc
; =============== S U B R O U T I N E =======================================
; Attributes: bp-based frame
sub_1261B proc far
var_E = word ptr -0Eh
var_C = word ptr -0Ch
var_A = word ptr -0Ah
var_8 = word ptr -8
var_6 = word ptr -6
@@bottom = word ptr -4
@@top = word ptr -2
enter 0Eh, 0
push si
push di
mov [bp+var_8], 0
call _z_vsync_wait_and_scrollup stdcall, 0
pop cx
mov ax, boss.BE_cur_top
add ax, 48
mov [bp+@@top], ax
mov ax, boss.BE_cur_top
add ax, 48
mov [bp+@@bottom], ax
loc_12640:
cmp [bp+@@top], 0
jl short loc_1264E
call @grcg_whiteline$qi stdcall, [bp+@@top]
pop cx
loc_1264E:
cmp [bp+@@bottom], (RES_Y - 1)
jg short loc_1265D
call @grcg_whiteline$qi stdcall, [bp+@@bottom]
pop cx
loc_1265D:
dec [bp+@@top]
inc [bp+@@bottom]
inc [bp+var_8]
cmp [bp+@@top], 0
jge short loc_12673
cmp [bp+@@bottom], (RES_Y - 1)
jg short loc_126C3
loc_12673:
mov ax, [bp+var_8]
mov bx, 15
cwd
idiv bx
or dx, dx
jnz short loc_126B8
xor si, si
jmp short loc_126A7
; ---------------------------------------------------------------------------
loc_12684:
xor di, di
jmp short loc_126A1
; ---------------------------------------------------------------------------
loc_12688:
mov bx, si
imul bx, 3
mov al, _z_Palettes[bx+di]
cbw
cmp ax, 0Fh
jge short loc_126A0
mov bx, si
imul bx, 3
inc byte ptr _z_Palettes[bx+di]
loc_126A0:
inc di
loc_126A1:
cmp di, 3
jl short loc_12688
inc si
loc_126A7:
cmp si, 10h
jl short loc_12684
call _z_palette_set_all_show c, offset _z_Palettes, ds
loc_126B8:
push 1
call _frame_delay
pop cx
jmp loc_12640
; ---------------------------------------------------------------------------
loc_126C3:
mov [bp+var_8], 0
xor si, si
jmp short loc_126E1
; ---------------------------------------------------------------------------
loc_126CC:
xor di, di
jmp short loc_126DB
; ---------------------------------------------------------------------------
loc_126D0:
mov bx, si
imul bx, 3
mov byte ptr _z_Palettes[bx+di], 0Fh
inc di
loc_126DB:
cmp di, 3
jl short loc_126D0
inc si
loc_126E1:
cmp si, 10h
jl short loc_126CC
call _z_palette_set_all_show stdcall, offset _z_Palettes, ds
push 1
call _graph_accesspage_func
call _graph_copy_accessed_page_to_othe
push 0
call _graph_accesspage_func
call _ptn_put_8 stdcall, _player_left, (PTN_MIKO_L shl 16) or _player_top
add sp, 0Eh
loc_12714:
inc [bp+var_8]
mov [bp+var_6], 0
mov ax, [bp+var_8]
mov bx, 15
cwd
idiv bx
or dx, dx
jnz short loc_1276B
xor si, si
jmp short loc_1275A
; ---------------------------------------------------------------------------
loc_1272D:
xor di, di
jmp short loc_12754
; ---------------------------------------------------------------------------
loc_12731:
mov bx, si
imul bx, size rgb_t
mov al, _z_Palettes[bx+di]
mov bx, si
imul bx, size rgb_t
cmp al, byte ptr _boss_post_defeat_palette[bx+di]
jle short loc_12750
mov bx, si
imul bx, size rgb_t
dec byte ptr _z_Palettes[bx+di]
jmp short loc_12753
; ---------------------------------------------------------------------------
loc_12750:
inc [bp+var_6]
loc_12753:
inc di
loc_12754:
cmp di, size rgb_t
jl short loc_12731
inc si
loc_1275A:
cmp si, COLOR_COUNT
jl short loc_1272D
call _z_palette_set_all_show c, offset _z_Palettes, ds
loc_1276B:
cmp [bp+var_6], 30h ; '0'
jge short loc_1277B
push 1
call _frame_delay
pop cx
jmp short loc_12714
; ---------------------------------------------------------------------------
loc_1277B:
xor si, si
jmp short loc_127AC
; ---------------------------------------------------------------------------
loc_1277F:
xor di, di
jmp short loc_127A6
; ---------------------------------------------------------------------------
loc_12783:
mov bx, si
imul bx, 3
mov al, _z_Palettes[bx+di]
cbw
or ax, ax
jle short loc_127A5
mov bx, si
imul bx, 3
mov al, _z_Palettes[bx+di]
add al, -4
mov bx, si
imul bx, 3
mov _z_Palettes[bx+di], al
loc_127A5:
inc di
loc_127A6:
cmp di, 3
jl short loc_12783
inc si
loc_127AC:
cmp si, 10h
jl short loc_1277F
mov _z_Palettes[7 * 3].r, 0Fh
mov _z_Palettes[7 * 3].g, 0Fh
mov _z_Palettes[7 * 3].b, 0Fh
call _z_palette_set_all_show stdcall, offset _z_Palettes, ds
push 1
call _graph_accesspage_func
call _z_graph_clear
call _graph_putsa_fx stdcall, 0, large ((FX_CLEAR_BG or 7) shl 16) or 0, offset aVqvnvtvmvcb@vp, ds ; "  "
call _graph_putsa_fx stdcall, 0, large ((FX_CLEAR_BG or 7) shl 16) or 32, offset aVbvpvovzvtvbvf, ds ; ""
call _graph_putsa_fx stdcall, 0, large ((FX_CLEAR_BG or 7) shl 16) or 64, offset aVrvevmvevgvfb@, ds ; "  "
call _graph_putsa_fx stdcall, 0, large ((FX_CLEAR_BG or 1) shl 16) or 96, offset aCvkev, ds ; "魔界へ"
add sp, 2Eh
call _graph_putsa_fx stdcall, 0, large ((FX_CLEAR_BG or 3) shl 16) or 128, offset aTnncv, ds ; "地獄へ"
push 0
call _graph_accesspage_func
push (0Fh shl 16) or 0Fh
push (0Fh shl 16) or 01h
call _z_palette_set_show
push (09h shl 16) or 09h
push (09h shl 16) or 03h
call _z_palette_set_show
call _graph_2xscale_byterect_1_to_0_sl stdcall, large ( 64 shl 16) or 64, large ( 0 shl 16) or 0, large (16 shl 16) or 208
add sp, 28h
call _graph_2xscale_byterect_1_to_0_sl stdcall, large ( 96 shl 16) or 32, large ( 32 shl 16) or 0, large (16 shl 16) or 256
call _graph_2xscale_byterect_1_to_0_sl stdcall, large (180 shl 16) or 32, large ( 64 shl 16) or 0, large (16 shl 16) or 272
call _graph_2xscale_byterect_1_to_0_sl stdcall, large (250 shl 16) or 256, large ( 96 shl 16) or 0, large (16 shl 16) or 48
call _graph_2xscale_byterect_1_to_0_sl stdcall, large (300 shl 16) or 256, large (128 shl 16) or 0, large (16 shl 16) or 48
add sp, 30h
call _graph_copy_accessed_page_to_othe
call _ptn_put_8 stdcall, 128, (3 shl 16) or 250
add sp, 6
mov [bp+var_A], 0
mov _input_shot, 0
mov _input_ok, 0
call _input_reset_sense
mov [bp+var_C], 0
mov [bp+var_E], 0
push 32h ; '2'
loc_12902:
call _frame_delay
pop cx
call _input_sense stdcall, 0
pop cx
cmp _input_up, 0
jz short loc_1291D
cmp [bp+var_C], 0
jz short loc_1292E
loc_1291D:
cmp _input_down, 0
jz loc_129C8
cmp [bp+var_E], 0
jnz loc_129C8
loc_1292E:
cmp [bp+var_A], 0
jnz short loc_1297F
call _egc_copy_rect_1_to_0_16 stdcall, large (250 shl 16) or 128, large (32 shl 16) or 32
call _ptn_put_8 stdcall, 128, (3 shl 16) or 300
push (0Fh shl 16) or 0Fh
push (0Fh shl 16) or 03h
call _z_palette_set_show
push (09h shl 16) or 09h
push (09h shl 16) or 01h
call _z_palette_set_show
add sp, 1Eh
mov [bp+var_A], 1
jmp short loc_129C8
; ---------------------------------------------------------------------------
loc_1297F:
call _egc_copy_rect_1_to_0_16 stdcall, large (300 shl 16) or 128, large (32 shl 16) or 32
call _ptn_put_8 stdcall, 128, (3 shl 16) or 250
push (0Fh shl 16) or 0Fh
push (0Fh shl 16) or 01h
call _z_palette_set_show
push (09h shl 16) or 09h
push (09h shl 16) or 03h
call _z_palette_set_show
add sp, 1Eh
mov [bp+var_A], 0
loc_129C8:
cmp _input_up, 0
jnz short loc_129D4
mov [bp+var_C], 0
loc_129D4:
cmp _input_down, 0
jnz short loc_129E0
mov [bp+var_E], 0
loc_129E0:
cmp _input_up, 0
jz short loc_129EC
mov [bp+var_C], 1
loc_129EC:
cmp _input_down, 0
jz short loc_129F8
mov [bp+var_E], 1
loc_129F8:
cmp _input_shot, 1
jz short loc_12A06
cmp _input_ok, 1
jnz short loc_12A18
loc_12A06:
push 4
call _mdrv2_se_play
pop cx
cmp [bp+var_A], 0
jnz short loc_12A1D
push 1
jmp short loc_12A1F
; ---------------------------------------------------------------------------
loc_12A18:
push 5
jmp loc_12902
; ---------------------------------------------------------------------------
loc_12A1D:
push 2
loc_12A1F:
call @scene_init_and_load$quc
pop cx
mov al, byte ptr [bp+var_A]
mov _route, al
mov _stage_cleared, 1
mov _done, 1
pop di
pop si
leave
retf
sub_1261B endp
extern @singyoku_defeat_animate_and_sele$qv:proc
; =============== S U B R O U T I N E =======================================
@ -15265,7 +14903,7 @@ loc_2326F:
call _mdrv2_bgm_fade_out_nonblock
call @CPellets@unput_and_reset$qv stdcall, offset _Pellets, ds
add sp, 0Eh
call sub_1261B
call @singyoku_defeat_animate_and_sele$qv
loc_232A0:
pop di
@ -30231,12 +29869,14 @@ include th01/sprites/ileave_m.asp
db 0FFh
include th01/sprites/laser_s.asp
include th01/snd/mdrv2[data].asm
aVqvnvtvmvcb@vp db '  ',0
aVbvpvovzvtvbvf db '',0
aVrvevmvevgvfb@ db '  ',0
aCvkev db '魔界へ',0
aTnncv db '地獄へ',0
db 0
public _ROUTE_SEL_1, _ROUTE_SEL_2, _ROUTE_SEL_3
public _ROUTE_SEL_4, _ROUTE_SEL_5
_ROUTE_SEL_1 db '  ',0
_ROUTE_SEL_2 db '',0
_ROUTE_SEL_3 db '  ',0
_ROUTE_SEL_4 db '魔界へ',0
_ROUTE_SEL_5 db '地獄へ',0
evendata
dword_3573A dd 0FFBBFFEEh
off_3573E dd aVo
; "O"