mirror of
https://github.com/coop-deluxe/sm64coopdx.git
synced 2026-05-07 17:31:41 +00:00
Add audio_stream_get/set_volume_channel (#1205)
Some checks are pending
Build coop / build-linux (push) Waiting to run
Build coop / build-steamos (push) Waiting to run
Build coop / build-windows-opengl (push) Waiting to run
Build coop / build-windows-directx (push) Waiting to run
Build coop / build-macos-arm (push) Waiting to run
Build coop / build-macos-intel (push) Waiting to run
Some checks are pending
Build coop / build-linux (push) Waiting to run
Build coop / build-steamos (push) Waiting to run
Build coop / build-windows-opengl (push) Waiting to run
Build coop / build-windows-directx (push) Waiting to run
Build coop / build-macos-arm (push) Waiting to run
Build coop / build-macos-intel (push) Waiting to run
Allows modders to play audio streams on channels other than level background music. 4 constants have been added for this purpose: - `MOD_AUDIO_CHANNEL_MASTER` - sound is only affected by master volume - `MOD_AUDIO_CHANNEL_MUSIC` - sound is affected by music volume, same as previous behaviour - `MOD_AUDIO_CHANNEL_SFX` - sound is affected by sfx volume, same as sample behaviour - `MOD_AUDIO_CHANNEL_ENV` - sound is affected by env volume This was done instead of using the existing `SEQ_PLAYER_*` constants to avoid confusion and because there isn't a `NONE`/`MASTER` option. Additionally, sets the default to `MOD_AUDIO_CHANNEL_MUSIC` as to not break compatibility. ```lua audio_stream_set_volume_channel(stream, MOD_AUDIO_CHANNEL_SFX) -- wow its just like a sample audio_stream_get_volume_channel(stream) -- returns MOD_AUDIO_CHANNEL_SFX (its actually 2) ```
This commit is contained in:
parent
de7ad4b0d6
commit
5dabcaa313
14 changed files with 195 additions and 19 deletions
|
|
@ -56,6 +56,7 @@ in_files = [
|
|||
"include/PR/gbi.h",
|
||||
"include/PR/gbi_extension.h",
|
||||
"src/engine/surface_load.h",
|
||||
"src/pc/lua/utils/smlua_audio_utils.h",
|
||||
]
|
||||
|
||||
exclude_constants = {
|
||||
|
|
@ -77,6 +78,7 @@ include_constants = {
|
|||
"include/geo_commands.h": [ "BACKGROUND" ],
|
||||
"include/level_commands.h": [ "WARP_CHECKPOINT", "WARP_NO_CHECKPOINT" ],
|
||||
"src/audio/external.h": [ "SEQ_PLAYER", "DS_" ],
|
||||
"src/pc/lua/utils/smlua_audio_utils.h": ["MOD_AUDIO_CHANNEL"],
|
||||
"src/pc/mods/mod_storage.h": [ "MAX_KEYS", "MAX_KEY_VALUE_LENGTH" ],
|
||||
"include/PR/gbi.h": [
|
||||
"^G_NOOP$",
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ override_field_invisible = {
|
|||
"FnGraphNode": [ "luaTokenIndex" ],
|
||||
"Object": [ "firstSurface" ],
|
||||
"Animation": [ "unusedBoneCount" ],
|
||||
"ModAudio": [ "sound", "decoder", "buffer", "bufferSize", "sampleCopiesTail" ],
|
||||
"ModAudio": [ "sound", "decoder", "buffer", "bufferSize", "sampleCopiesTail", "volChannel" ],
|
||||
"Painting": [ "normalDisplayList", "textureMaps", "rippleDisplayList", "ripples" ],
|
||||
"DialogEntry": [ "str" ],
|
||||
"ModFsFile": [ "data", "capacity" ],
|
||||
|
|
|
|||
|
|
@ -8171,6 +8171,18 @@ VALID_BUTTONS = (A_BUTTON | B_BUTTON | Z_TRIG | START_BUTTON | U_JPAD | D_JPAD |
|
|||
--- @type integer
|
||||
C_BUTTONS = (U_CBUTTONS | D_CBUTTONS | L_CBUTTONS | R_CBUTTONS )
|
||||
|
||||
--- @type integer
|
||||
MOD_AUDIO_CHANNEL_MASTER = 0
|
||||
|
||||
--- @type integer
|
||||
MOD_AUDIO_CHANNEL_MUSIC = 1
|
||||
|
||||
--- @type integer
|
||||
MOD_AUDIO_CHANNEL_SFX = 2
|
||||
|
||||
--- @type integer
|
||||
MOD_AUDIO_CHANNEL_ENV = 3
|
||||
|
||||
HOOK_UPDATE = 0 --- @type LuaHookedEventType
|
||||
HOOK_MARIO_UPDATE = 1 --- @type LuaHookedEventType
|
||||
HOOK_BEFORE_MARIO_UPDATE = 2 --- @type LuaHookedEventType
|
||||
|
|
|
|||
|
|
@ -10334,6 +10334,20 @@ function audio_stream_set_volume(audio, volume)
|
|||
-- ...
|
||||
end
|
||||
|
||||
--- @param audio ModAudio
|
||||
--- @return integer
|
||||
--- Gets the volume channel of an `audio` stream
|
||||
function audio_stream_get_volume_channel(audio)
|
||||
-- ...
|
||||
end
|
||||
|
||||
--- @param audio ModAudio
|
||||
--- @param channel integer
|
||||
--- Sets the volume channel of an `audio` stream
|
||||
function audio_stream_set_volume_channel(audio, channel)
|
||||
-- ...
|
||||
end
|
||||
|
||||
--- @param filename string
|
||||
--- @return ModAudio
|
||||
--- Loads an `audio` sample
|
||||
|
|
|
|||
|
|
@ -1212,6 +1212,7 @@
|
|||
--- @field public looping boolean
|
||||
--- @field public frequency number
|
||||
--- @field public volume number
|
||||
--- @field public channel integer
|
||||
|
||||
--- @class ModFs
|
||||
--- @field public mod Mod
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
- [seq_ids.h](#seq_idsh)
|
||||
- [enum SeqId](#enum-SeqId)
|
||||
- [sm64.h](#sm64h)
|
||||
- [smlua_audio_utils.h](#smlua_audio_utilsh)
|
||||
- [smlua_hooks.h](#smlua_hooksh)
|
||||
- [enum LuaHookedEventType](#enum-LuaHookedEventType)
|
||||
- [enum LuaHookedEventReturn](#enum-LuaHookedEventReturn)
|
||||
|
|
@ -3486,6 +3487,16 @@
|
|||
|
||||
<br />
|
||||
|
||||
## [smlua_audio_utils.h](#smlua_audio_utils.h)
|
||||
- MOD_AUDIO_CHANNEL_MASTER
|
||||
- MOD_AUDIO_CHANNEL_MUSIC
|
||||
- MOD_AUDIO_CHANNEL_SFX
|
||||
- MOD_AUDIO_CHANNEL_ENV
|
||||
|
||||
[:arrow_up_small:](#)
|
||||
|
||||
<br />
|
||||
|
||||
## [smlua_hooks.h](#smlua_hooks.h)
|
||||
|
||||
### [enum LuaHookedEventType](#LuaHookedEventType)
|
||||
|
|
|
|||
|
|
@ -5947,6 +5947,53 @@ Sets the volume of an `audio` stream
|
|||
|
||||
<br />
|
||||
|
||||
## [audio_stream_get_volume_channel](#audio_stream_get_volume_channel)
|
||||
|
||||
### Description
|
||||
Gets the volume channel of an `audio` stream
|
||||
|
||||
### Lua Example
|
||||
`local integerValue = audio_stream_get_volume_channel(audio)`
|
||||
|
||||
### Parameters
|
||||
| Field | Type |
|
||||
| ----- | ---- |
|
||||
| audio | [ModAudio](structs.md#ModAudio) |
|
||||
|
||||
### Returns
|
||||
- `integer`
|
||||
|
||||
### C Prototype
|
||||
`u8 audio_stream_get_volume_channel(struct ModAudio *audio);`
|
||||
|
||||
[:arrow_up_small:](#)
|
||||
|
||||
<br />
|
||||
|
||||
## [audio_stream_set_volume_channel](#audio_stream_set_volume_channel)
|
||||
|
||||
### Description
|
||||
Sets the volume channel of an `audio` stream
|
||||
|
||||
### Lua Example
|
||||
`audio_stream_set_volume_channel(audio, channel)`
|
||||
|
||||
### Parameters
|
||||
| Field | Type |
|
||||
| ----- | ---- |
|
||||
| audio | [ModAudio](structs.md#ModAudio) |
|
||||
| channel | `integer` |
|
||||
|
||||
### Returns
|
||||
- None
|
||||
|
||||
### C Prototype
|
||||
`void audio_stream_set_volume_channel(struct ModAudio *audio, u8 channel);`
|
||||
|
||||
[:arrow_up_small:](#)
|
||||
|
||||
<br />
|
||||
|
||||
## [audio_sample_load](#audio_sample_load)
|
||||
|
||||
### Description
|
||||
|
|
|
|||
|
|
@ -1849,6 +1849,8 @@
|
|||
- [audio_stream_set_frequency](functions-6.md#audio_stream_set_frequency)
|
||||
- [audio_stream_get_volume](functions-6.md#audio_stream_get_volume)
|
||||
- [audio_stream_set_volume](functions-6.md#audio_stream_set_volume)
|
||||
- [audio_stream_get_volume_channel](functions-6.md#audio_stream_get_volume_channel)
|
||||
- [audio_stream_set_volume_channel](functions-6.md#audio_stream_set_volume_channel)
|
||||
- [audio_sample_load](functions-6.md#audio_sample_load)
|
||||
- [audio_sample_destroy](functions-6.md#audio_sample_destroy)
|
||||
- [audio_sample_stop](functions-6.md#audio_sample_stop)
|
||||
|
|
|
|||
|
|
@ -1768,6 +1768,7 @@
|
|||
| looping | `boolean` | |
|
||||
| frequency | `number` | |
|
||||
| volume | `number` | |
|
||||
| channel | `integer` | |
|
||||
|
||||
[:arrow_up_small:](#)
|
||||
|
||||
|
|
|
|||
|
|
@ -1494,18 +1494,19 @@ static struct LuaObjectField sModFields[LUA_MOD_FIELD_COUNT] = {
|
|||
{ "size", LVT_U64, offsetof(struct Mod, size), true, LOT_NONE },
|
||||
};
|
||||
|
||||
#define LUA_MOD_AUDIO_FIELD_COUNT 10
|
||||
#define LUA_MOD_AUDIO_FIELD_COUNT 11
|
||||
static struct LuaObjectField sModAudioFields[LUA_MOD_AUDIO_FIELD_COUNT] = {
|
||||
{ "baseVolume", LVT_F32, offsetof(struct ModAudio, baseVolume), false, LOT_NONE },
|
||||
{ "file", LVT_PROPERTY, .get = "return_self" },
|
||||
{ "filepath", LVT_STRING_P, offsetof(struct ModAudio, filepath), true, LOT_NONE },
|
||||
{ "frequency", LVT_PROPERTY, .get = "audio_stream_get_frequency", .set = "audio_stream_set_frequency" },
|
||||
{ "isStream", LVT_BOOL, offsetof(struct ModAudio, isStream), true, LOT_NONE },
|
||||
{ "loaded", LVT_BOOL, offsetof(struct ModAudio, loaded), true, LOT_NONE },
|
||||
{ "looping", LVT_PROPERTY, .get = "audio_stream_get_looping", .set = "audio_stream_set_looping" },
|
||||
{ "position", LVT_PROPERTY, .get = "audio_stream_get_position", .set = "audio_stream_set_position" },
|
||||
{ "relativePath", LVT_STRING_P, offsetof(struct ModAudio, relativePath), true, LOT_NONE },
|
||||
{ "volume", LVT_PROPERTY, .get = "audio_stream_get_volume", .set = "audio_stream_set_volume" },
|
||||
{ "baseVolume", LVT_F32, offsetof(struct ModAudio, baseVolume), false, LOT_NONE },
|
||||
{ "channel", LVT_PROPERTY, .get = "audio_stream_get_volume_channel", .set = "audio_stream_set_volume_channel" },
|
||||
{ "file", LVT_PROPERTY, .get = "return_self" },
|
||||
{ "filepath", LVT_STRING_P, offsetof(struct ModAudio, filepath), true, LOT_NONE },
|
||||
{ "frequency", LVT_PROPERTY, .get = "audio_stream_get_frequency", .set = "audio_stream_set_frequency" },
|
||||
{ "isStream", LVT_BOOL, offsetof(struct ModAudio, isStream), true, LOT_NONE },
|
||||
{ "loaded", LVT_BOOL, offsetof(struct ModAudio, loaded), true, LOT_NONE },
|
||||
{ "looping", LVT_PROPERTY, .get = "audio_stream_get_looping", .set = "audio_stream_set_looping" },
|
||||
{ "position", LVT_PROPERTY, .get = "audio_stream_get_position", .set = "audio_stream_set_position" },
|
||||
{ "relativePath", LVT_STRING_P, offsetof(struct ModAudio, relativePath), true, LOT_NONE },
|
||||
{ "volume", LVT_PROPERTY, .get = "audio_stream_get_volume", .set = "audio_stream_set_volume" },
|
||||
};
|
||||
|
||||
#define LUA_MOD_FS_FIELD_COUNT 15
|
||||
|
|
|
|||
|
|
@ -3495,6 +3495,10 @@ char gSmluaConstants[] = ""
|
|||
"ACT_RELEASING_BOWSER=0x00000392\n"
|
||||
"VALID_BUTTONS=(A_BUTTON | B_BUTTON | Z_TRIG | START_BUTTON | U_JPAD | D_JPAD | L_JPAD | R_JPAD | L_TRIG | R_TRIG | X_BUTTON | Y_BUTTON | U_CBUTTONS | D_CBUTTONS | L_CBUTTONS | R_CBUTTONS )\n"
|
||||
"C_BUTTONS=(U_CBUTTONS | D_CBUTTONS | L_CBUTTONS | R_CBUTTONS )\n"
|
||||
"MOD_AUDIO_CHANNEL_MASTER=0\n"
|
||||
"MOD_AUDIO_CHANNEL_MUSIC=1\n"
|
||||
"MOD_AUDIO_CHANNEL_SFX=2\n"
|
||||
"MOD_AUDIO_CHANNEL_ENV=3\n"
|
||||
"HOOK_UPDATE=0\n"
|
||||
"HOOK_MARIO_UPDATE=1\n"
|
||||
"HOOK_BEFORE_MARIO_UPDATE=2\n"
|
||||
|
|
|
|||
|
|
@ -30774,6 +30774,42 @@ int smlua_func_audio_stream_set_volume(lua_State* L) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
int smlua_func_audio_stream_get_volume_channel(lua_State* L) {
|
||||
if (L == NULL) { return 0; }
|
||||
|
||||
int top = lua_gettop(L);
|
||||
if (top != 1) {
|
||||
LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "audio_stream_get_volume_channel", 1, top);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct ModAudio* audio = (struct ModAudio*)smlua_to_cobject(L, 1, LOT_MODAUDIO);
|
||||
if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "audio_stream_get_volume_channel"); return 0; }
|
||||
|
||||
lua_pushinteger(L, audio_stream_get_volume_channel(audio));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int smlua_func_audio_stream_set_volume_channel(lua_State* L) {
|
||||
if (L == NULL) { return 0; }
|
||||
|
||||
int top = lua_gettop(L);
|
||||
if (top != 2) {
|
||||
LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "audio_stream_set_volume_channel", 2, top);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct ModAudio* audio = (struct ModAudio*)smlua_to_cobject(L, 1, LOT_MODAUDIO);
|
||||
if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "audio_stream_set_volume_channel"); return 0; }
|
||||
u8 channel = smlua_to_integer(L, 2);
|
||||
if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 2, "audio_stream_set_volume_channel"); return 0; }
|
||||
|
||||
audio_stream_set_volume_channel(audio, channel);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int smlua_func_audio_sample_load(lua_State* L) {
|
||||
if (L == NULL) { return 0; }
|
||||
|
||||
|
|
@ -38680,6 +38716,8 @@ void smlua_bind_functions_autogen(void) {
|
|||
smlua_bind_function(L, "audio_stream_set_frequency", smlua_func_audio_stream_set_frequency);
|
||||
smlua_bind_function(L, "audio_stream_get_volume", smlua_func_audio_stream_get_volume);
|
||||
smlua_bind_function(L, "audio_stream_set_volume", smlua_func_audio_stream_set_volume);
|
||||
smlua_bind_function(L, "audio_stream_get_volume_channel", smlua_func_audio_stream_get_volume_channel);
|
||||
smlua_bind_function(L, "audio_stream_set_volume_channel", smlua_func_audio_stream_set_volume_channel);
|
||||
smlua_bind_function(L, "audio_sample_load", smlua_func_audio_sample_load);
|
||||
smlua_bind_function(L, "audio_sample_destroy", smlua_func_audio_sample_destroy);
|
||||
smlua_bind_function(L, "audio_sample_stop", smlua_func_audio_sample_stop);
|
||||
|
|
|
|||
|
|
@ -363,10 +363,24 @@ struct ModAudio* audio_load_internal(const char* filename, bool isStream) {
|
|||
audio->buffer = buffer;
|
||||
audio->bufferSize = size;
|
||||
audio->isStream = isStream;
|
||||
audio->baseVolume = 1.0f;
|
||||
audio->volChannel = MOD_AUDIO_CHANNEL_MUSIC;
|
||||
audio->loaded = true;
|
||||
return audio;
|
||||
}
|
||||
|
||||
static f32 get_audio_volume(struct ModAudio* audio) {
|
||||
f32 volume = audio->baseVolume;
|
||||
if (audio->volChannel == MOD_AUDIO_CHANNEL_MUSIC) {
|
||||
volume *= (f32)configMusicVolume / 127.0f * (f32)gLuaVolumeLevel / 127.0f;
|
||||
} else if (audio->volChannel == MOD_AUDIO_CHANNEL_SFX) {
|
||||
volume *= (f32)configSfxVolume / 127.0f * (f32)gLuaVolumeSfx / 127.0f;
|
||||
} else if (audio->volChannel == MOD_AUDIO_CHANNEL_ENV) {
|
||||
volume *= (f32)configEnvVolume / 127.0f * (f32)gLuaVolumeEnv / 127.0f;
|
||||
}
|
||||
return gMasterVolume * volume;
|
||||
}
|
||||
|
||||
struct ModAudio* audio_stream_load(const char* filename) {
|
||||
return audio_load_internal(filename, true);
|
||||
}
|
||||
|
|
@ -388,6 +402,7 @@ void audio_stream_play(struct ModAudio* audio, bool restart, f32 volume) {
|
|||
ma_sound_set_volume(&audio->sound, gMasterVolume * musicVolume * volume);
|
||||
}
|
||||
audio->baseVolume = volume;
|
||||
ma_sound_set_volume(&audio->sound, get_audio_volume(audio));
|
||||
if (restart || !ma_sound_is_playing(&audio->sound)) { ma_sound_seek_to_pcm_frame(&audio->sound, 0); }
|
||||
ma_sound_start(&audio->sound);
|
||||
}
|
||||
|
|
@ -473,14 +488,9 @@ f32 audio_stream_get_volume(struct ModAudio* audio) {
|
|||
|
||||
void audio_stream_set_volume(struct ModAudio* audio, f32 volume) {
|
||||
if (!audio_sanity_check(audio, true, "set stream volume for")) { return; }
|
||||
|
||||
if (configMuteFocusLoss && !WAPI.has_focus()) {
|
||||
ma_sound_set_volume(&audio->sound, 0);
|
||||
} else {
|
||||
f32 musicVolume = (f32)configMusicVolume / 127.0f * (f32)gLuaVolumeLevel / 127.0f;
|
||||
ma_sound_set_volume(&audio->sound, gMasterVolume * musicVolume * volume);
|
||||
}
|
||||
|
||||
audio->baseVolume = volume;
|
||||
ma_sound_set_volume(&audio->sound, get_audio_volume(audio));
|
||||
}
|
||||
|
||||
// void audio_stream_set_speed(struct ModAudio* audio, f32 initial_freq, f32 speed, bool pitch) {
|
||||
|
|
@ -489,6 +499,28 @@ void audio_stream_set_volume(struct ModAudio* audio, f32 volume) {
|
|||
// bassh_set_speed(audio->handle, initial_freq, speed, pitch);
|
||||
// }
|
||||
|
||||
u8 audio_stream_get_volume_channel(struct ModAudio* audio) {
|
||||
if (!audio_sanity_check(audio, true, "get stream volume channel from")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return audio->volChannel;
|
||||
}
|
||||
|
||||
void audio_stream_set_volume_channel(struct ModAudio* audio, u8 channel) {
|
||||
if (!audio_sanity_check(audio, true, "set stream volume channel for")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel > MOD_AUDIO_CHANNEL_ENV) {
|
||||
LOG_LUA_LINE("Tried to set volume channel to invalid value: %d", channel);
|
||||
return;
|
||||
}
|
||||
|
||||
audio->volChannel = channel;
|
||||
ma_sound_set_volume(&audio->sound, get_audio_volume(audio));
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
|
||||
// MA calls the end callback from its audio thread
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ u8 smlua_audio_utils_allocate_sequence(void);
|
|||
// mod sounds //
|
||||
////////////////
|
||||
|
||||
#define MOD_AUDIO_CHANNEL_MASTER 0
|
||||
#define MOD_AUDIO_CHANNEL_MUSIC 1
|
||||
#define MOD_AUDIO_CHANNEL_SFX 2
|
||||
#define MOD_AUDIO_CHANNEL_ENV 3
|
||||
|
||||
struct ModAudioSampleCopies {
|
||||
ma_sound sound;
|
||||
ma_decoder decoder;
|
||||
|
|
@ -35,12 +40,14 @@ struct ModAudio {
|
|||
struct ModAudioSampleCopies* sampleCopiesTail;
|
||||
bool isStream;
|
||||
f32 baseVolume;
|
||||
u8 volChannel;
|
||||
bool loaded;
|
||||
|
||||
PROPERTY(position, audio_stream_get_position, audio_stream_set_position);
|
||||
PROPERTY(looping, audio_stream_get_looping, audio_stream_set_looping);
|
||||
PROPERTY(frequency, audio_stream_get_frequency, audio_stream_set_frequency);
|
||||
PROPERTY(volume, audio_stream_get_volume, audio_stream_set_volume);
|
||||
PROPERTY(channel, audio_stream_get_volume_channel, audio_stream_set_volume_channel);
|
||||
|
||||
PROPERTY(file, return_self, NULL); // compatibility band-aid
|
||||
};
|
||||
|
|
@ -73,6 +80,10 @@ void audio_stream_set_frequency(struct ModAudio* audio, f32 freq);
|
|||
f32 audio_stream_get_volume(struct ModAudio* audio);
|
||||
/* |description|Sets the volume of an `audio` stream|descriptionEnd| */
|
||||
void audio_stream_set_volume(struct ModAudio* audio, f32 volume);
|
||||
/* |description|Gets the volume channel of an `audio` stream|descriptionEnd| */
|
||||
u8 audio_stream_get_volume_channel(struct ModAudio *audio);
|
||||
/* |description|Sets the volume channel of an `audio` stream|descriptionEnd| */
|
||||
void audio_stream_set_volume_channel(struct ModAudio *audio, u8 channel);
|
||||
|
||||
void audio_sample_destroy_pending_copies(void);
|
||||
/* |description|Loads an `audio` sample|descriptionEnd| */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue