Raphael Texier 7b4a99cba8
Adding Localization support (#159)
* Innitial commit

Localization for retro-go using a simple 0(n) lookup function called rg_gettext()

* adding language settings in options menu

* adding more gettext()

* new lookup function

* adding "For these changes to take effect you must restart your device." gui alert + fixing gettext() function

* modifying the gui dialog

* updating struct syntax

* update struct syntax (again)

* creating the python tool for localization

* updating tool + adding missing translations

* moving stuff to libs + starting writing readme

* adding missing "libs/localization" folder import in cmakelist + added the "fixme for rg_system"

* synthax adjust + moving back stuff from libs to retro-go

* removing trailing spaces

* adding the enum for language ids

* updating documentation according to the latest changes

* small tweaks

* Moved LOCALIZATION.md to the root folder

Whilst it is mostly relevant to libretro-go, it really is project-wide documentation.

* rg_localization: Got rid of the switch, made GUI dynamic

This makes adding a language more straightforward.

I kept the *msg *fr *en for now to avoid updating translations.h, but it could be replaced by the GCC extension as such:

[RG_LANG_EN] = "...",
[RG_LANG_FR] = "...",

So that adding a language is really just updating the enum...

* rg_localization: translations is const, we can use RG_COUNT

* rg_gui: Fixed language selection

* rg_localization: No need to validate rg_language in rg_gettext

It should always be valid, there's no need to validate it.

* rg_gui: Show language name in the log

* rg_localization: Got rid of the Translation struct

I am not 100% positive this is a good move...

Benefits:
- One less thing to change when adding a language
- Less code is always better

Cons:
- It doesn't make it clear what the "key" is (the english text)
- If in the future we need to add things like flags it will have to be returned to a struct

* updated python tool + updating translations

* added missing translations

* audio filter wrong translation

* fix : "a propose de retro-go"

---------

Co-authored-by: Alex Duchesne <ducalex007@gmail.com>
2024-11-16 13:04:50 -05:00

585 lines
15 KiB
C

/*
* This file is part of doom-ng-odroid-go.
* Copyright (c) 2019 ducalex.
*
* 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, version 3.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <sys/dirent.h>
#include <sys/unistd.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <doomtype.h>
#include <doomstat.h>
#include <doomdef.h>
#include <d_main.h>
#include <g_game.h>
#include <i_system.h>
#include <i_video.h>
#include <i_sound.h>
#include <i_main.h>
#include <m_argv.h>
#include <m_fixed.h>
#include <m_misc.h>
#include <r_draw.h>
#include <r_fps.h>
#include <s_sound.h>
#include <st_stuff.h>
#include <mus2mid.h>
#include <midifile.h>
#include <oplplayer.h>
#include <rg_system.h>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#endif
#define AUDIO_SAMPLE_RATE 22050
#define AUDIO_BUFFER_LENGTH (AUDIO_SAMPLE_RATE / TICRATE + 1)
#define NUM_MIX_CHANNELS 8
static rg_surface_t *update;
static rg_app_t *app;
static const char *doom_argv[10];
// Expected variables by doom
int snd_card = 1, mus_card = 1;
int snd_samplerate = AUDIO_SAMPLE_RATE;
int current_palette = 0;
typedef struct {
uint16_t unused1;
uint16_t samplerate;
uint16_t length;
uint16_t unused2;
byte samples[];
} doom_sfx_t;
typedef struct {
const doom_sfx_t *sfx;
size_t pos;
float factor;
int starttic;
} channel_t;
static channel_t channels[NUM_MIX_CHANNELS];
static const doom_sfx_t *sfx[NUMSFX];
static rg_audio_sample_t mixbuffer[AUDIO_BUFFER_LENGTH];
static const music_player_t *music_player = &opl_synth_player;
static bool musicPlaying = false;
// TO DO: Detect when menu is open so we can send better keys.
static const struct {int mask; int *key;} keymap[] = {
{RG_KEY_UP, &key_up},
{RG_KEY_DOWN, &key_down},
{RG_KEY_LEFT, &key_left},
{RG_KEY_RIGHT, &key_right},
{RG_KEY_A, &key_fire},
{RG_KEY_A, &key_enter},
{RG_KEY_B, &key_speed},
{RG_KEY_B, &key_strafe},
{RG_KEY_B, &key_backspace},
{RG_KEY_MENU, &key_escape},
{RG_KEY_OPTION, &key_map},
{RG_KEY_START, &key_use},
{RG_KEY_SELECT, &key_weapontoggle},
};
static const char *SETTING_GAMMA = "Gamma";
static rg_gui_event_t gamma_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
int gamma = usegamma;
int max = 9;
if (event == RG_DIALOG_PREV)
gamma = gamma > 0 ? gamma - 1 : max;
if (event == RG_DIALOG_NEXT)
gamma = gamma < max ? gamma + 1 : 0;
if (gamma != usegamma)
{
usegamma = gamma;
rg_settings_set_number(NS_APP, SETTING_GAMMA, gamma);
I_SetPalette(current_palette);
return RG_DIALOG_REDRAW;
}
sprintf(option->value, "%d/%d", gamma, max);
return RG_DIALOG_VOID;
}
void I_StartFrame(void)
{
//
}
void I_UpdateNoBlit(void)
{
//
}
void I_FinishUpdate(void)
{
rg_display_submit(update, 0);
rg_display_sync(true); // Wait for update->buffer to be released
}
bool I_StartDisplay(void)
{
return true;
}
void I_EndDisplay(void)
{
//
}
void I_SetPalette(int pal)
{
uint16_t *palette = V_BuildPalette(pal, 16);
for (int i = 0; i < 256; i++)
update->palette[i] = palette[i] << 8 | palette[i] >> 8;
Z_Free(palette);
current_palette = pal;
}
void I_InitGraphics(void)
{
// set first three to standard values
for (int i = 0; i < 3; i++)
{
screens[i].width = SCREENWIDTH;
screens[i].height = SCREENHEIGHT;
screens[i].byte_pitch = SCREENWIDTH;
}
// Main screen uses internal ram for speed
screens[0].data = update->data;
screens[0].not_on_heap = true;
// statusbar
screens[4].width = SCREENWIDTH;
screens[4].height = (ST_SCALED_HEIGHT + 1);
screens[4].byte_pitch = SCREENWIDTH;
}
int I_GetTimeMS(void)
{
return rg_system_timer() / 1000;
}
int I_GetTime(void)
{
return I_GetTimeMS() * TICRATE * realtic_clock_rate / 100000;
}
void I_uSleep(unsigned long usecs)
{
rg_usleep(usecs);
}
void I_SafeExit(int rc)
{
rg_system_exit();
}
const char *I_DoomExeDir(void)
{
return RG_BASE_PATH_ROMS "/doom";
}
void I_UpdateSoundParams(int handle, int volume, int seperation, int pitch)
{
}
int I_StartSound(int sfxid, int channel, int vol, int sep, int pitch, int priority)
{
int oldest = gametic;
int slot = 0;
// Unknown sound
if (!sfx[sfxid])
return -1;
// These sound are played only once at a time. Stop any running ones.
if (sfxid == sfx_sawup || sfxid == sfx_sawidl || sfxid == sfx_sawful
|| sfxid == sfx_sawhit || sfxid == sfx_stnmov || sfxid == sfx_pistol)
{
for (int i = 0; i < NUM_MIX_CHANNELS; i++)
{
if (channels[i].sfx == sfx[sfxid])
channels[i].sfx = NULL;
}
}
// Find available channel or steal the oldest
for (int i = 0; i < NUM_MIX_CHANNELS; i++)
{
if (channels[i].sfx == NULL)
{
slot = i;
break;
}
else if (channels[i].starttic < oldest)
{
slot = i;
oldest = channels[i].starttic;
}
}
channel_t *chan = &channels[slot];
chan->sfx = sfx[sfxid];
chan->factor = (float)chan->sfx->samplerate / snd_samplerate;
chan->pos = 0;
return slot;
}
void I_StopSound(int handle)
{
if (handle < NUM_MIX_CHANNELS)
channels[handle].sfx = NULL;
}
bool I_SoundIsPlaying(int handle)
{
// return (handle < NUM_MIX_CHANNELS && channels[handle].sfx);
return false;
}
bool I_AnySoundStillPlaying(void)
{
for (int i = 0; i < NUM_MIX_CHANNELS; i++)
if (channels[i].sfx)
return true;
return false;
}
static void soundTask(void *arg)
{
while (1)
{
bool haveMusic = snd_MusicVolume > 0 && musicPlaying;
bool haveSFX = snd_SfxVolume > 0 && I_AnySoundStillPlaying();
if (haveMusic)
{
music_player->render(mixbuffer, AUDIO_BUFFER_LENGTH);
}
if (haveSFX)
{
int16_t *audioBuffer = (int16_t *)mixbuffer;
int16_t *audioBufferEnd = audioBuffer + AUDIO_BUFFER_LENGTH * 2;
while (audioBuffer < audioBufferEnd)
{
int totalSample = 0;
int totalSources = 0;
int sample;
for (int i = 0; i < NUM_MIX_CHANNELS; i++)
{
channel_t *chan = &channels[i];
if (!chan->sfx)
continue;
size_t pos = (size_t)(chan->pos++ * chan->factor);
if (pos >= chan->sfx->length)
{
chan->sfx = NULL;
}
else if ((sample = chan->sfx->samples[pos]))
{
totalSample += sample - 127;
totalSources++;
}
}
totalSample <<= 7;
totalSample /= (16 - snd_SfxVolume);
if (haveMusic)
{
totalSample += *audioBuffer;
totalSources += (totalSources == 0);
}
if (totalSources > 0)
totalSample /= totalSources;
if (totalSample > 32767)
totalSample = 32767;
else if (totalSample < -32768)
totalSample = -32768;
*audioBuffer++ = totalSample;
*audioBuffer++ = totalSample;
}
}
if (!haveMusic && !haveSFX)
{
memset(mixbuffer, 0, sizeof(mixbuffer));
}
rg_audio_submit(mixbuffer, AUDIO_BUFFER_LENGTH);
}
}
void I_InitSound(void)
{
for (int i = 1; i < NUMSFX; i++)
{
if (S_sfx[i].lumpnum != -1)
sfx[i] = W_CacheLumpNum(S_sfx[i].lumpnum);
}
music_player->init(snd_samplerate);
music_player->setvolume(snd_MusicVolume);
rg_task_create("doom_sound", &soundTask, NULL, 2048, RG_TASK_PRIORITY_2, 1);
}
void I_ShutdownSound(void)
{
music_player->shutdown();
}
void I_PlaySong(int handle, int looping)
{
music_player->play((void *)handle, looping);
musicPlaying = true;
}
void I_PauseSong(int handle)
{
music_player->pause();
musicPlaying = false;
}
void I_ResumeSong(int handle)
{
music_player->resume();
musicPlaying = true;
}
void I_StopSong(int handle)
{
music_player->stop();
musicPlaying = false;
}
void I_UnRegisterSong(int handle)
{
music_player->unregistersong((void *)handle);
}
int I_RegisterSong(const void *data, size_t len)
{
uint8_t *mid = NULL;
size_t midlen;
int handle = 0;
if (mus2mid(data, len, &mid, &midlen, 64) == 0)
handle = (int)music_player->registersong(mid, midlen);
else
handle = (int)music_player->registersong(data, len);
free(mid);
return handle;
}
void I_SetMusicVolume(int volume)
{
music_player->setvolume(volume);
}
void I_StartTic(void)
{
static int64_t last_time = 0;
static int32_t prev_joystick = 0x0000;
static int32_t rg_menu_delay = 0;
uint32_t joystick = rg_input_read_gamepad();
uint32_t changed = prev_joystick ^ joystick;
event_t event = {0};
// Long press on menu will open retro-go's menu if needed, instead of DOOM's.
// This is still needed to quit (DOOM 2) and for the debug menu. We'll unify that mess soon...
if (joystick & (RG_KEY_MENU|RG_KEY_OPTION))
{
if (joystick & RG_KEY_OPTION)
{
Z_FreeTags(PU_CACHE, PU_CACHE); // At this point the heap is usually full. Let's reclaim some!
rg_gui_options_menu();
changed = 0;
}
else if (rg_menu_delay++ == TICRATE / 2)
{
Z_FreeTags(PU_CACHE, PU_CACHE); // At this point the heap is usually full. Let's reclaim some!
rg_gui_game_menu();
}
realtic_clock_rate = app->speed * 100;
R_InitInterpolation();
}
else
{
rg_menu_delay = 0;
}
if (changed)
{
for (int i = 0; i < RG_COUNT(keymap); i++)
{
if (changed & keymap[i].mask)
{
event.type = (joystick & keymap[i].mask) ? ev_keydown : ev_keyup;
event.data1 = *keymap[i].key;
D_PostEvent(&event);
}
}
}
rg_system_tick(rg_system_timer() - last_time);
last_time = rg_system_timer();
prev_joystick = joystick;
}
void I_Init(void)
{
snd_channels = NUM_MIX_CHANNELS;
snd_samplerate = AUDIO_SAMPLE_RATE;
snd_MusicVolume = 15;
snd_SfxVolume = 15;
usegamma = rg_settings_get_number(NS_APP, SETTING_GAMMA, 0);
}
static bool screenshot_handler(const char *filename, int width, int height)
{
Z_FreeTags(PU_CACHE, PU_CACHE); // At this point the heap is usually full. Let's reclaim some!
return rg_surface_save_image_file(update, filename, width, height);
}
static bool save_state_handler(const char *filename)
{
rg_gui_alert("Not implemented", "Please use the in-game menu");
return false;
}
static bool load_state_handler(const char *filename)
{
rg_gui_alert("Not implemented", "Please use the in-game menu");
return false;
}
static bool reset_handler(bool hard)
{
return false;
}
static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_SHUTDOWN)
{
// DOOM fully fills the internal heap and this causes some shutdown
// steps to fail so we try to free everything!
Z_FreeTags(0, PU_MAX);
rg_audio_set_mute(true);
}
else if (event == RG_EVENT_REDRAW)
{
rg_display_submit(update, 0);
}
}
bool is_iwad(const char *path)
{
char header[16] = {0};
void *data = &header;
size_t data_len = 16;
if (rg_extension_match(path, "zip"))
rg_storage_unzip_file(path, NULL, &data, &data_len, RG_FILE_USER_BUFFER);
else
rg_storage_read_file(path, &data, &data_len, RG_FILE_USER_BUFFER);
return header[0] == 'I' && header[1] == 'W';
}
void app_main()
{
const rg_handlers_t handlers = {
.loadState = &load_state_handler,
.saveState = &save_state_handler,
.reset = &reset_handler,
.screenshot = &screenshot_handler,
.event = &event_handler,
};
const rg_gui_option_t options[] = {
{0, _("Gamma Boost"), "-", RG_DIALOG_FLAG_NORMAL, &gamma_update_cb},
RG_DIALOG_END
};
app = rg_system_init(AUDIO_SAMPLE_RATE, &handlers, options);
app->tickRate = TICRATE;
const rg_display_t *display = rg_display_get_info();
SCREENWIDTH = RG_MIN(display->screen.width, MAX_SCREENWIDTH);
SCREENHEIGHT = RG_MIN(display->screen.height, MAX_SCREENHEIGHT);
update = rg_surface_create(SCREENWIDTH, SCREENHEIGHT, RG_PIXEL_PAL565_BE, MEM_FAST);
const char *iwad = NULL;
const char *pwad = NULL;
if (is_iwad(app->romPath))
iwad = app->romPath;
else
pwad = app->romPath;
if (!iwad)
{
iwad = rg_gui_file_picker("Select IWAD file", I_DoomExeDir(), is_iwad, false) ?: "";
rg_gui_draw_hourglass(); // Redraw hourglass to indicate loading...
}
myargv = doom_argv;
myargc = pwad ? 7 : 5;
doom_argv[0] = "doom";
doom_argv[1] = "-save";
doom_argv[2] = RG_BASE_PATH_SAVES "/doom";
doom_argv[3] = "-iwad";
doom_argv[4] = iwad;
doom_argv[5] = "-file";
doom_argv[6] = pwad;
doom_argv[myargc] = 0;
#ifdef ESP_PLATFORM
// Some things might be nice to place in internal RAM, but I do not have time to find such
// structures. So for now, prefer external RAM for most things except the framebuffer which
// is allocated above.
heap_caps_malloc_extmem_enable(0);
#endif
Z_Init();
D_DoomMain();
}