Compare commits

..

9 Commits

Author SHA1 Message Date
Storm Dragon
1000736389 License added. 2026-03-03 00:38:50 -05:00
Storm Dragon
e999b2ff5c Latest fixes for multilingual support. 2026-02-27 23:47:21 -05:00
Storm Dragon
c658bb6cd7 Translate learn sounds labels before descriptions 2026-02-27 23:43:36 -05:00
Storm Dragon
a69d47b879 Added my format settings. 2026-02-25 19:42:30 -05:00
Storm Dragon
e749618afd Character dialog added. First attempt, let's see what happens. 2026-02-25 19:40:18 -05:00
Storm Dragon
59f2880498 Latest changes. 2026-02-24 23:11:47 -05:00
Storm Dragon
7531eacf64 Merge branch 'master' of ssh://git.stormux.org:1101/storm/libstorm-nvgt 2026-02-22 19:27:50 -05:00
Storm Dragon
8825bc38d7 Updated several components. I really gotta remember I split this off into a submodule. 2026-02-22 19:27:16 -05:00
Storm Dragon
aa1ab8f533 Batch update libstorm-nvgt modules 2026-02-22 19:18:47 -05:00
21 changed files with 1580 additions and 1058 deletions

18
.clang-format Normal file
View File

@@ -0,0 +1,18 @@
---
BasedOnStyle: LLVM
IndentWidth: 4
ContinuationIndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 120
BreakBeforeBraces: Attach
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
IndentCaseLabels: true
SortIncludes: Never
ReflowComments: false
PointerAlignment: Left
ReferenceAlignment: Left
SpaceBeforeParens: ControlStatements
...

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.wtfpl.net/txt/copying/">here</a>.</p>
<hr>
<address>Apache/2.4.66 (Debian) Server at www.wtfpl.net Port 80</address>
</body></html>

View File

@@ -9,6 +9,7 @@ Reusable NVGT helpers for Storm projects.
- `text_reader_compat.nvgt`: Optional aliases (`text_reader*`) for legacy code.
- `ui.nvgt`: Dialog wrappers with optional main-window restoration.
- `speech_history.nvgt`: `speak_with_history()` wrapper plus comma/period history navigation.
- `character_dialog.nvgt`: Blocking dialog lines with repeat/next/skip keys and optional per-word pre-speech sounds.
- `notifications.nvgt`: Queued notifications with history and optional sound playback.
- `menu_helpers.nvgt`: Simple menu runner + filter helpers with `sounds/menu` defaults.
- `menu_music.nvgt`: Reusable menu music start/pause/stop helpers with blocking fade-out pause by default.
@@ -29,6 +30,7 @@ Reusable NVGT helpers for Storm projects.
#include "libstorm-nvgt/text_reader_compat.nvgt" // Optional legacy aliases
#include "libstorm-nvgt/ui.nvgt"
#include "libstorm-nvgt/speech_history.nvgt"
#include "libstorm-nvgt/character_dialog.nvgt"
#include "libstorm-nvgt/notifications.nvgt"
#include "libstorm-nvgt/menu_helpers.nvgt"
#include "libstorm-nvgt/menu_music.nvgt"
@@ -110,6 +112,22 @@ void stop_main_menu_music() {
file_viewer_file("README.txt", "Readme", true);
```
## character_dialog Example
```nvgt
#include "libstorm-nvgt/character_dialog.nvgt"
void play_intro_dialog() {
string[] lines = {"Welcome to the gate.", "State your name and purpose."};
character_dialog_show_lines(lines);
}
```
```nvgt
// Optional override sound. Custom paths play once per line instead of once per word.
character_dialog_set_sound_path("sounds/cutscene/chime");
```
## speech_history + notifications Example
```nvgt
@@ -140,7 +158,8 @@ void on_event() {
## learn_sounds Example (Project Override File)
```nvgt
// In your project root, create excluded_sounds.nvgt:
// The override file can be named and placed anywhere in your project.
// Example file: src/sound_settings.nvgt
void configure_project_learn_sounds() {
learn_sounds_reset_configuration();
learn_sounds_add_skip_entry("sounds/menu/");
@@ -151,7 +170,7 @@ void configure_project_learn_sounds() {
```nvgt
// In your game module:
#include "libstorm-nvgt/learn_sounds.nvgt"
#include "excluded_sounds.nvgt"
#include "src/sound_settings.nvgt"
#include "libstorm-nvgt/speech_history.nvgt"
void learn_sounds_bridge_speak(const string &in message, bool interrupt) {

View File

@@ -3,23 +3,23 @@
// Resolves an audio path with optional extension fallback.
// Checks exact path first, then ".ogg", then ".wav".
string resolve_audio_path(const string audioPath) {
if (audioPath == "") {
return "";
}
if (audioPath == "") {
return "";
}
if (file_exists(audioPath)) {
return audioPath;
}
if (file_exists(audioPath)) {
return audioPath;
}
string oggPath = audioPath + ".ogg";
if (file_exists(oggPath)) {
return oggPath;
}
string oggPath = audioPath + ".ogg";
if (file_exists(oggPath)) {
return oggPath;
}
string wavPath = audioPath + ".wav";
if (file_exists(wavPath)) {
return wavPath;
}
string wavPath = audioPath + ".wav";
if (file_exists(wavPath)) {
return wavPath;
}
return "";
return "";
}

201
character_dialog.nvgt Normal file
View File

@@ -0,0 +1,201 @@
funcdef void character_dialog_speak_callback(const string& in message, bool interrupt);
string characterDialogSoundPath = "sounds/dialog";
bool characterDialogShowUsageInstructions = true;
bool characterDialogUsageInstructionsShown = false;
bool characterDialogSoundLoaded = false;
sound characterDialogSound;
character_dialog_speak_callback @characterDialogSpeakCallback = null;
void character_dialog_set_speak_callback(character_dialog_speak_callback @callback) {
@characterDialogSpeakCallback = @callback;
}
void character_dialog_set_sound_path(const string soundPath) {
characterDialogSoundPath = soundPath;
characterDialogSoundLoaded = false;
characterDialogSound.close();
}
void character_dialog_set_show_usage_instructions(bool enabled) {
characterDialogShowUsageInstructions = enabled;
}
void character_dialog_reset_usage_instructions() {
characterDialogUsageInstructionsShown = false;
}
void character_dialog_reset() {
characterDialogUsageInstructionsShown = false;
characterDialogSoundLoaded = false;
characterDialogSound.close();
}
void character_dialog_speak(const string& in message, bool interrupt) {
if (@characterDialogSpeakCallback !is null) {
characterDialogSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
}
bool character_dialog_should_skip() {
return key_pressed(KEY_ESCAPE) || key_pressed(KEY_AC_BACK);
}
bool character_dialog_ensure_sound_loaded() {
if (characterDialogSoundLoaded) {
return true;
}
characterDialogSound.close();
if (characterDialogSound.load(characterDialogSoundPath)) {
characterDialogSoundLoaded = true;
return true;
}
if (characterDialogSound.load(characterDialogSoundPath + ".ogg")) {
characterDialogSoundLoaded = true;
return true;
}
if (!characterDialogSound.load(characterDialogSoundPath + ".wav")) {
return false;
}
characterDialogSoundLoaded = true;
return true;
}
bool character_dialog_use_word_sound_sequence() {
if (characterDialogSoundPath == "sounds/dialog" || characterDialogSoundPath == "sounds/dialog.ogg" ||
characterDialogSoundPath == "sounds/dialog.wav") {
return true;
}
return false;
}
int character_dialog_count_words(const string& in message) {
if (message == "") {
return 0;
}
int wordCount = 0;
bool inWord = false;
for (uint charIndex = 0; charIndex < message.length(); charIndex++) {
string character = message.substr(charIndex, 1);
bool isWhitespace = character == " " || character == "\t" || character == "\n" || character == "\r";
if (isWhitespace) {
inWord = false;
continue;
}
if (!inWord) {
wordCount++;
inWord = true;
}
}
return wordCount;
}
bool character_dialog_play_word_sound_sequence(const string& in message) {
if (!character_dialog_ensure_sound_loaded()) {
return false;
}
int playCount = 1;
if (character_dialog_use_word_sound_sequence()) {
playCount = character_dialog_count_words(message);
}
for (int wordIndex = 0; wordIndex < playCount; wordIndex++) {
characterDialogSound.play();
while (characterDialogSound.playing) {
wait(5);
if (character_dialog_should_skip()) {
return true;
}
}
}
return false;
}
bool character_dialog_repeat_requested() {
key_code[] @pressedKeys = keys_pressed();
if (@pressedKeys is null || pressedKeys.length() == 0)
return false;
for (uint keyIndex = 0; keyIndex < pressedKeys.length(); keyIndex++) {
int keyCode = pressedKeys[keyIndex];
if (keyCode == KEY_RETURN || keyCode == KEY_NUMPAD_ENTER || keyCode == KEY_ESCAPE || keyCode == KEY_AC_BACK)
continue;
return true;
}
return false;
}
int character_dialog_wait_for_action(const string& in currentLine, bool interrupt) {
while (true) {
wait(5);
if (character_dialog_should_skip()) {
return -1;
}
if (key_pressed(KEY_RETURN) || key_pressed(KEY_NUMPAD_ENTER)) {
return 1;
}
bool repeatRequested = character_dialog_repeat_requested();
if (repeatRequested) {
character_dialog_speak(currentLine, interrupt);
}
}
return -1;
}
bool character_dialog_show_lines(const string[] @dialogLines, bool interrupt = true) {
if (@dialogLines is null || dialogLines.length() == 0) {
return true;
}
for (uint lineIndex = 0; lineIndex < dialogLines.length(); lineIndex++) {
string currentLine = dialogLines[lineIndex];
if (currentLine == "") {
continue;
}
if (character_dialog_play_word_sound_sequence(currentLine)) {
character_dialog_speak(" ", true);
return false;
}
string displayLine = currentLine;
if (characterDialogShowUsageInstructions && !characterDialogUsageInstructionsShown) {
displayLine += " Press enter to continue, escape to skip, or any other key to repeat.";
characterDialogUsageInstructionsShown = true;
}
character_dialog_speak(displayLine, interrupt);
int action = character_dialog_wait_for_action(currentLine, interrupt);
if (action < 0) {
character_dialog_speak(" ", true);
return false;
}
}
character_dialog_speak(" ", true);
return true;
}
void character_dialog_show(const string[] @dialogLines, bool interrupt = true) {
/* The orange goblins speak to me in the night,
as the moon casts shadows pumpkins come to life! */
character_dialog_show_lines(dialogLines, interrupt);
}

View File

@@ -1,58 +1,64 @@
// Crash and log helpers.
string log_format_timestamp() {
datetime dt;
string stamp = dt.format(DATE_TIME_FORMAT_RFC1123);
return "[" + stamp + "]";
datetime dt;
string stamp = dt.format(DATE_TIME_FORMAT_RFC1123);
return "[" + stamp + "]";
}
string log_flatten_multiline(const string&in text) {
string result = "";
bool lastWasSeparator = false;
string log_flatten_multiline(const string& in text) {
string result = "";
bool lastWasSeparator = false;
for (uint charIndex = 0; charIndex < text.length(); charIndex++) {
string ch = text.substr(charIndex, 1);
if (ch == "\r" || ch == "\n") {
if (!lastWasSeparator) {
result += " | ";
lastWasSeparator = true;
}
continue;
}
for (uint charIndex = 0; charIndex < text.length(); charIndex++) {
string ch = text.substr(charIndex, 1);
if (ch == "\r" || ch == "\n") {
if (!lastWasSeparator) {
result += " | ";
lastWasSeparator = true;
}
continue;
}
result += ch;
lastWasSeparator = false;
}
result += ch;
lastWasSeparator = false;
}
return result;
return result;
}
bool log_append_line(const string&in logPath, const string&in message) {
file logFile;
if (!logFile.open(logPath, "ab")) {
return false;
}
bool log_append_line(const string& in logPath, const string& in message) {
file logFile;
if (!logFile.open(logPath, "ab")) {
return false;
}
// Keep log format as message then timestamp.
logFile.write(message + " " + log_format_timestamp() + "\r\n");
logFile.close();
return true;
// Keep log format as message then timestamp.
logFile.write(message + " " + log_format_timestamp() + "\r\n");
logFile.close();
return true;
}
void log_unhandled_exception_to_file(const string&in logPath = "crash.log", const string&in context = "") {
string info = get_exception_info();
string filePath = get_exception_file();
int line = get_exception_line();
string func = get_exception_function();
string stack = log_flatten_multiline(last_exception_call_stack);
void log_unhandled_exception_to_file(const string& in logPath = "crash.log", const string& in context = "") {
string info = get_exception_info();
string filePath = get_exception_file();
int line = get_exception_line();
string func = get_exception_function();
string stack = log_flatten_multiline(last_exception_call_stack);
string message = "Unhandled exception";
if (context != "") message += " (" + context + ")";
if (info != "") message += ": " + info;
if (filePath != "") message += " at " + filePath;
if (line > 0) message += ":" + line;
if (func != "") message += " in " + func;
if (stack != "") message += " | stack: " + stack;
string message = "Unhandled exception";
if (context != "")
message += " (" + context + ")";
if (info != "")
message += ": " + info;
if (filePath != "")
message += " at " + filePath;
if (line > 0)
message += ":" + line;
if (func != "")
message += " in " + func;
if (stack != "")
message += " | stack: " + stack;
log_append_line(logPath, message);
log_append_line(logPath, message);
}

View File

@@ -1,58 +1,72 @@
// Dictionary utility helpers.
double dict_get_number(dictionary@ data, const string&in key, double defaultValue) {
double value = 0.0;
if (@data is null) return defaultValue;
if (data.get(key, value)) return value;
double dict_get_number(dictionary @data, const string& in key, double defaultValue) {
double value = 0.0;
if (@data is null)
return defaultValue;
if (data.get(key, value))
return value;
int valueInt = 0;
if (data.get(key, valueInt)) return valueInt;
int valueInt = 0;
if (data.get(key, valueInt))
return valueInt;
string valueString = "";
if (data.get(key, valueString)) {
return parse_int(valueString);
}
string valueString = "";
if (data.get(key, valueString)) {
return parse_int(valueString);
}
return defaultValue;
return defaultValue;
}
bool dict_get_bool(dictionary@ data, const string&in key, bool defaultValue) {
bool value = false;
if (@data is null) return defaultValue;
if (data.get(key, value)) return value;
bool dict_get_bool(dictionary @data, const string& in key, bool defaultValue) {
bool value = false;
if (@data is null)
return defaultValue;
if (data.get(key, value))
return value;
int valueInt = 0;
if (data.get(key, valueInt)) return valueInt != 0;
int valueInt = 0;
if (data.get(key, valueInt))
return valueInt != 0;
string valueString = "";
if (data.get(key, valueString)) return valueString == "1" || valueString == "true";
string valueString = "";
if (data.get(key, valueString))
return valueString == "1" || valueString == "true";
return defaultValue;
return defaultValue;
}
bool dict_has_keys(dictionary@ data) {
if (@data is null) return false;
string[]@ keys = data.get_keys();
return @keys !is null && keys.length() > 0;
bool dict_has_keys(dictionary @data) {
if (@data is null)
return false;
string[] @keys = data.get_keys();
return @keys !is null && keys.length() > 0;
}
bool dict_has_number_key(dictionary@ data, const string&in key) {
double value = 0.0;
if (@data is null) return false;
if (data.get(key, value)) return true;
bool dict_has_number_key(dictionary @data, const string& in key) {
double value = 0.0;
if (@data is null)
return false;
if (data.get(key, value))
return true;
int valueInt = 0;
if (data.get(key, valueInt)) return true;
int valueInt = 0;
if (data.get(key, valueInt))
return true;
string valueString = "";
if (data.get(key, valueString)) return valueString.length() > 0;
string valueString = "";
if (data.get(key, valueString))
return valueString.length() > 0;
return false;
return false;
}
string[] dict_get_string_list(dictionary@ data, const string&in key) {
string[] result;
if (@data is null) return result;
if (!data.get(key, result)) return result;
return result;
string[] dict_get_string_list(dictionary @data, const string& in key) {
string[] result;
if (@data is null)
return result;
if (!data.get(key, result))
return result;
return result;
}

View File

@@ -1,11 +1,11 @@
#include "audio_paths.nvgt"
#include "file_viewer.nvgt"
funcdef void docs_browser_speak_callback(const string &in message, bool interrupt);
funcdef void docs_browser_speak_callback(const string& in message, bool interrupt);
funcdef void docs_browser_tick_callback();
docs_browser_speak_callback@ docsBrowserSpeakCallback = null;
docs_browser_tick_callback@ docsBrowserTickCallback = null;
docs_browser_speak_callback @docsBrowserSpeakCallback = null;
docs_browser_tick_callback @docsBrowserTickCallback = null;
string docsBrowserDirectory = "files";
string docsBrowserMenuSoundDir = "sounds/menu";
@@ -16,189 +16,198 @@ string[] docsBrowserExtensions = {"md", "MD", "txt", "TXT"};
sound docsBrowserMoveSound;
sound docsBrowserSelectSound;
void docs_browser_set_speak_callback(docs_browser_speak_callback@ callback) {
@docsBrowserSpeakCallback = @callback;
void docs_browser_set_speak_callback(docs_browser_speak_callback @callback) {
@docsBrowserSpeakCallback = @callback;
}
void docs_browser_set_tick_callback(docs_browser_tick_callback@ callback) {
@docsBrowserTickCallback = @callback;
void docs_browser_set_tick_callback(docs_browser_tick_callback @callback) {
@docsBrowserTickCallback = @callback;
}
void docs_browser_set_docs_dir(const string docsDir) {
docsBrowserDirectory = docsDir;
docsBrowserDirectory = docsDir;
}
void docs_browser_set_menu_sound_dir(const string menuSoundDir) {
docsBrowserMenuSoundDir = menuSoundDir;
docsBrowserMenuSoundDir = menuSoundDir;
}
void docs_browser_set_wrap(bool wrap) {
docsBrowserWrap = wrap;
docsBrowserWrap = wrap;
}
void docs_browser_set_extensions(string[]@ extensions) {
docsBrowserExtensions.resize(0);
if (@extensions is null) return;
for (uint extIndex = 0; extIndex < extensions.length(); extIndex++) {
docsBrowserExtensions.insert_last(extensions[extIndex]);
}
void docs_browser_set_extensions(string[] @extensions) {
docsBrowserExtensions.resize(0);
if (@extensions is null)
return;
for (uint extIndex = 0; extIndex < extensions.length(); extIndex++) {
docsBrowserExtensions.insert_last(extensions[extIndex]);
}
}
void docs_browser_reset_default_extensions() {
string[] defaults = {"md", "MD", "txt", "TXT"};
docs_browser_set_extensions(defaults);
string[] defaults = {"md", "MD", "txt", "TXT"};
docs_browser_set_extensions(defaults);
}
void docs_browser_speak(const string &in message, bool interrupt) {
if (@docsBrowserSpeakCallback !is null) {
docsBrowserSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
void docs_browser_speak(const string& in message, bool interrupt) {
if (@docsBrowserSpeakCallback !is null) {
docsBrowserSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
}
void docs_browser_tick() {
if (@docsBrowserTickCallback !is null) {
docsBrowserTickCallback();
}
if (@docsBrowserTickCallback !is null) {
docsBrowserTickCallback();
}
}
bool docs_browser_sort_case_insensitive(const string &in a, const string &in b) {
return a.lower() < b.lower();
bool docs_browser_sort_case_insensitive(const string& in a, const string& in b) {
return a.lower() < b.lower();
}
void docs_browser_append_unique_case_insensitive(string[]@ items, const string&in value) {
string lowerValue = value.lower();
for (uint itemIndex = 0; itemIndex < items.length(); itemIndex++) {
if (items[itemIndex].lower() == lowerValue) return;
}
items.insert_last(value);
void docs_browser_append_unique_case_insensitive(string[] @items, const string& in value) {
string lowerValue = value.lower();
for (uint itemIndex = 0; itemIndex < items.length(); itemIndex++) {
if (items[itemIndex].lower() == lowerValue)
return;
}
items.insert_last(value);
}
string docs_browser_collapse_spaces(const string&in text) {
string result = text;
while (result.find_first(" ") > -1) {
result = result.replace(" ", " ", true);
}
return result;
string docs_browser_collapse_spaces(const string& in text) {
string result = text;
while (result.find_first(" ") > -1) {
result = result.replace(" ", " ", true);
}
return result;
}
string docs_browser_format_label(const string&in filename) {
string name = filename;
int dotPos = name.find_last_of(".");
if (dotPos > 0) {
name = name.substr(0, dotPos);
}
string docs_browser_format_label(const string& in filename) {
string name = filename;
int dotPos = name.find_last_of(".");
if (dotPos > 0) {
name = name.substr(0, dotPos);
}
name = name.replace("_", " ", true);
name = name.replace("-", " ", true);
name = docs_browser_collapse_spaces(name);
name = name.lower();
name.trim_whitespace_this();
name = name.replace("_", " ", true);
name = name.replace("-", " ", true);
name = docs_browser_collapse_spaces(name);
name = name.lower();
name.trim_whitespace_this();
if (name.length() == 0) return "Document";
string first = name.substr(0, 1).upper();
if (name.length() == 1) return first;
return first + name.substr(1);
if (name.length() == 0)
return "Document";
string first = name.substr(0, 1).upper();
if (name.length() == 1)
return first;
return first + name.substr(1);
}
void docs_browser_collect_entries(string[]@ labels, string[]@ paths) {
labels.resize(0);
paths.resize(0);
void docs_browser_collect_entries(string[] @labels, string[] @paths) {
labels.resize(0);
paths.resize(0);
if (!directory_exists(docsBrowserDirectory)) return;
if (!directory_exists(docsBrowserDirectory))
return;
string[] docFiles;
for (uint extIndex = 0; extIndex < docsBrowserExtensions.length(); extIndex++) {
string ext = docsBrowserExtensions[extIndex];
if (ext == "") continue;
if (ext.substr(0, 1) == ".") {
ext = ext.substr(1);
}
string[] docFiles;
for (uint extIndex = 0; extIndex < docsBrowserExtensions.length(); extIndex++) {
string ext = docsBrowserExtensions[extIndex];
if (ext == "")
continue;
if (ext.substr(0, 1) == ".") {
ext = ext.substr(1);
}
string[]@ found = find_files(docsBrowserDirectory + "/*." + ext);
if (@found is null) continue;
for (uint fileIndex = 0; fileIndex < found.length(); fileIndex++) {
docs_browser_append_unique_case_insensitive(docFiles, found[fileIndex]);
}
}
string[] @found = find_files(docsBrowserDirectory + "/*." + ext);
if (@found is null)
continue;
for (uint fileIndex = 0; fileIndex < found.length(); fileIndex++) {
docs_browser_append_unique_case_insensitive(docFiles, found[fileIndex]);
}
}
if (docFiles.length() > 1) {
docFiles.sort(docs_browser_sort_case_insensitive);
}
if (docFiles.length() > 1) {
docFiles.sort(docs_browser_sort_case_insensitive);
}
for (uint fileIndex = 0; fileIndex < docFiles.length(); fileIndex++) {
labels.insert_last(docs_browser_format_label(docFiles[fileIndex]));
paths.insert_last(docsBrowserDirectory + "/" + docFiles[fileIndex]);
}
for (uint fileIndex = 0; fileIndex < docFiles.length(); fileIndex++) {
labels.insert_last(docs_browser_format_label(docFiles[fileIndex]));
paths.insert_last(docsBrowserDirectory + "/" + docFiles[fileIndex]);
}
}
void docs_browser_add_entries(string[]@ labels, string[]@ paths, int[]@ types, const int typeValue = 1) {
string[] docLabels;
string[] docPaths;
docs_browser_collect_entries(docLabels, docPaths);
void docs_browser_add_entries(string[] @labels, string[] @paths, int[] @types, const int typeValue = 1) {
string[] docLabels;
string[] docPaths;
docs_browser_collect_entries(docLabels, docPaths);
for (uint entryIndex = 0; entryIndex < docLabels.length(); entryIndex++) {
labels.insert_last(docLabels[entryIndex]);
paths.insert_last(docPaths[entryIndex]);
types.insert_last(typeValue);
}
for (uint entryIndex = 0; entryIndex < docLabels.length(); entryIndex++) {
labels.insert_last(docLabels[entryIndex]);
paths.insert_last(docPaths[entryIndex]);
types.insert_last(typeValue);
}
}
void docs_browser_play_ui_sound(sound &inout soundObj, const string basePath) {
string soundPath = resolve_audio_path(basePath);
if (soundPath == "") return;
void docs_browser_play_ui_sound(sound& inout soundObj, const string basePath) {
string soundPath = resolve_audio_path(basePath);
if (soundPath == "")
return;
soundObj.close();
if (!soundObj.load(soundPath)) return;
soundObj.play();
soundObj.close();
if (!soundObj.load(soundPath))
return;
soundObj.play();
}
void docs_browser_run_menu(const string menuTitle = "Documents") {
string[] labels;
string[] paths;
docs_browser_collect_entries(labels, paths);
string[] labels;
string[] paths;
docs_browser_collect_entries(labels, paths);
docs_browser_speak(menuTitle + ".", true);
if (labels.length() == 0) {
docs_browser_speak("No documents found.", true);
return;
}
docs_browser_speak(menuTitle + ".", true);
if (labels.length() == 0) {
docs_browser_speak("No documents found.", true);
return;
}
int selection = 0;
docs_browser_speak(labels[selection], true);
int selection = 0;
docs_browser_speak(labels[selection], true);
while (true) {
wait(5);
docs_browser_tick();
while (true) {
wait(5);
docs_browser_tick();
if (key_pressed(KEY_ESCAPE)) {
docs_browser_speak("Closed.", true);
return;
}
if (key_pressed(KEY_ESCAPE)) {
docs_browser_speak("Closed.", true);
return;
}
if (key_pressed(KEY_DOWN)) {
docs_browser_play_ui_sound(docsBrowserMoveSound, docsBrowserMenuSoundDir + "/menu_move");
selection++;
if (selection >= int(labels.length())) {
selection = docsBrowserWrap ? 0 : int(labels.length()) - 1;
}
docs_browser_speak(labels[selection], true);
}
if (key_pressed(KEY_DOWN)) {
docs_browser_play_ui_sound(docsBrowserMoveSound, docsBrowserMenuSoundDir + "/menu_move");
selection++;
if (selection >= int(labels.length())) {
selection = docsBrowserWrap ? 0 : int(labels.length()) - 1;
}
docs_browser_speak(labels[selection], true);
}
if (key_pressed(KEY_UP)) {
docs_browser_play_ui_sound(docsBrowserMoveSound, docsBrowserMenuSoundDir + "/menu_move");
selection--;
if (selection < 0) {
selection = docsBrowserWrap ? int(labels.length()) - 1 : 0;
}
docs_browser_speak(labels[selection], true);
}
if (key_pressed(KEY_UP)) {
docs_browser_play_ui_sound(docsBrowserMoveSound, docsBrowserMenuSoundDir + "/menu_move");
selection--;
if (selection < 0) {
selection = docsBrowserWrap ? int(labels.length()) - 1 : 0;
}
docs_browser_speak(labels[selection], true);
}
if (key_pressed(KEY_RETURN)) {
docs_browser_play_ui_sound(docsBrowserSelectSound, docsBrowserMenuSoundDir + "/menu_select");
file_viewer_file(paths[selection], labels[selection], true);
docs_browser_speak(labels[selection], true);
}
}
if (key_pressed(KEY_RETURN)) {
docs_browser_play_ui_sound(docsBrowserSelectSound, docsBrowserMenuSoundDir + "/menu_select");
file_viewer_file(paths[selection], labels[selection], true);
docs_browser_speak(labels[selection], true);
}
}
}

View File

@@ -3,83 +3,76 @@
// Reusable text/file viewer based on NVGT audio_form.
// Returns edited text in edit mode when user confirms.
string file_viewer(string content, string title = "Text Reader", bool readOnly = true) {
audio_form f;
f.create_window(title, false, true);
audio_form f;
f.create_window(title, false, true);
int textControl = f.create_input_box(
(readOnly ? "Document (read only)" : "Document (editable)"),
content,
"",
0,
readOnly,
true,
true
);
int textControl = f.create_input_box((readOnly ? "Document (read only)" : "Document (editable)"), content, "", 0,
readOnly, true, true);
int okButton = -1;
int closeButton = -1;
if (readOnly) {
closeButton = f.create_button("&Close", true, true);
} else {
okButton = f.create_button("&OK", true);
closeButton = f.create_button("&Cancel", false, true);
}
int okButton = -1;
int closeButton = -1;
if (readOnly) {
closeButton = f.create_button("&Close", true, true);
} else {
okButton = f.create_button("&OK", true);
closeButton = f.create_button("&Cancel", false, true);
}
f.focus(textControl);
f.focus(textControl);
while (true) {
f.monitor();
wait(5);
while (true) {
f.monitor();
wait(5);
if (!readOnly && okButton != -1 && f.is_pressed(okButton)) {
return f.get_text(textControl);
}
if (!readOnly && okButton != -1 && f.is_pressed(okButton)) {
return f.get_text(textControl);
}
if (closeButton != -1 && f.is_pressed(closeButton)) {
return "";
}
if (closeButton != -1 && f.is_pressed(closeButton)) {
return "";
}
if (key_pressed(KEY_ESCAPE)) {
return "";
}
}
if (key_pressed(KEY_ESCAPE)) {
return "";
}
}
return "";
return "";
}
string file_viewer_lines(string[] lines, string title = "Text Reader", bool readOnly = true) {
string content = join(lines, "\n");
return file_viewer(content, title, readOnly);
string content = join(lines, "\n");
return file_viewer(content, title, readOnly);
}
// Opens a file in the viewer. In edit mode, saves on confirm.
// Returns edited text when saved or empty string when canceled/failure.
string file_viewer_file(string filePath, string title = "", bool readOnly = true) {
file f;
if (!f.open(filePath, "rb")) {
screen_reader_speak("Failed to open file: " + filePath, true);
return "";
}
file f;
if (!f.open(filePath, "rb")) {
screen_reader_speak("Failed to open file: " + filePath, true);
return "";
}
string content = f.read();
f.close();
string content = f.read();
f.close();
if (title == "") {
title = filePath;
}
if (title == "") {
title = filePath;
}
string result = file_viewer(content, title, readOnly);
string result = file_viewer(content, title, readOnly);
if (!readOnly && result != "") {
if (f.open(filePath, "wb")) {
f.write(result);
f.close();
screen_reader_speak("File saved successfully", true);
return result;
}
if (!readOnly && result != "") {
if (f.open(filePath, "wb")) {
f.write(result);
f.close();
screen_reader_speak("File saved successfully", true);
return result;
}
screen_reader_speak("Failed to save file", true);
}
screen_reader_speak("Failed to save file", true);
}
return result;
return result;
}

View File

@@ -1,12 +1,12 @@
#include "audio_paths.nvgt"
funcdef void learn_sounds_speak_callback(const string &in message, bool interrupt);
funcdef void learn_sounds_speak_callback(const string& in message, bool interrupt);
funcdef void learn_sounds_tick_callback();
funcdef void learn_sounds_setup_callback();
learn_sounds_speak_callback@ learnSoundsSpeakCallback = null;
learn_sounds_tick_callback@ learnSoundsTickCallback = null;
learn_sounds_setup_callback@ learnSoundsSetupCallback = null;
learn_sounds_speak_callback @learnSoundsSpeakCallback = null;
learn_sounds_tick_callback @learnSoundsTickCallback = null;
learn_sounds_setup_callback @learnSoundsSetupCallback = null;
string[] learnSoundsSkipList;
string[] learnSoundsDescriptionPaths;
@@ -22,319 +22,390 @@ sound learnSoundsPreviewSound;
sound learnSoundsMoveSound;
sound learnSoundsSelectSound;
void learn_sounds_set_speak_callback(learn_sounds_speak_callback@ callback) {
@learnSoundsSpeakCallback = @callback;
void learn_sounds_set_speak_callback(learn_sounds_speak_callback @callback) {
@learnSoundsSpeakCallback = @callback;
}
void learn_sounds_set_tick_callback(learn_sounds_tick_callback@ callback) {
@learnSoundsTickCallback = @callback;
void learn_sounds_set_tick_callback(learn_sounds_tick_callback @callback) {
@learnSoundsTickCallback = @callback;
}
void learn_sounds_set_setup_callback(learn_sounds_setup_callback@ callback) {
@learnSoundsSetupCallback = @callback;
learnSoundsSetupApplied = false;
void learn_sounds_set_setup_callback(learn_sounds_setup_callback @callback) {
@learnSoundsSetupCallback = @callback;
learnSoundsSetupApplied = false;
}
void learn_sounds_set_root_dir(const string rootDir) {
learnSoundsRootDir = rootDir;
learnSoundsRootDir = rootDir;
}
void learn_sounds_set_menu_sound_dir(const string menuSoundDir) {
learnSoundsMenuSoundDir = menuSoundDir;
learnSoundsMenuSoundDir = menuSoundDir;
}
void learn_sounds_set_wrap(bool wrap) {
learnSoundsWrap = wrap;
learnSoundsWrap = wrap;
}
void learn_sounds_set_play_select_sound(bool playSelectSound) {
learnSoundsPlaySelectSound = playSelectSound;
learnSoundsPlaySelectSound = playSelectSound;
}
void learn_sounds_clear_skip_entries() {
learnSoundsSkipList.resize(0);
learnSoundsSkipList.resize(0);
}
void learn_sounds_add_skip_entry(const string entry) {
if (entry == "") {
return;
}
learnSoundsSkipList.insert_last(entry);
if (entry == "") {
return;
}
learnSoundsSkipList.insert_last(entry);
}
void learn_sounds_clear_descriptions() {
learnSoundsDescriptionPaths.resize(0);
learnSoundsDescriptionTexts.resize(0);
learnSoundsDescriptionPaths.resize(0);
learnSoundsDescriptionTexts.resize(0);
}
void learn_sounds_add_description(const string soundPath, const string description) {
if (soundPath == "") {
return;
}
learnSoundsDescriptionPaths.insert_last(soundPath);
learnSoundsDescriptionTexts.insert_last(description);
if (soundPath == "") {
return;
}
learnSoundsDescriptionPaths.insert_last(soundPath);
learnSoundsDescriptionTexts.insert_last(description);
}
void learn_sounds_reset_configuration() {
learn_sounds_clear_skip_entries();
learn_sounds_clear_descriptions();
learnSoundsSetupApplied = false;
learn_sounds_clear_skip_entries();
learn_sounds_clear_descriptions();
learnSoundsSetupApplied = false;
}
void learn_sounds_speak(const string &in message, bool interrupt) {
if (@learnSoundsSpeakCallback !is null) {
learnSoundsSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
void learn_sounds_speak(const string& in message, bool interrupt) {
if (@learnSoundsSpeakCallback !is null) {
learnSoundsSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
}
void learn_sounds_tick() {
if (@learnSoundsTickCallback !is null) {
learnSoundsTickCallback();
}
if (@learnSoundsTickCallback !is null) {
learnSoundsTickCallback();
}
}
void learn_sounds_apply_setup_once() {
if (learnSoundsSetupApplied) {
return;
}
if (learnSoundsSetupApplied) {
return;
}
if (@learnSoundsSetupCallback !is null) {
learnSoundsSetupCallback();
}
if (@learnSoundsSetupCallback !is null) {
learnSoundsSetupCallback();
}
learnSoundsSetupApplied = true;
learnSoundsSetupApplied = true;
}
string learn_sounds_normalize_path(const string&in path) {
return path.replace("\\", "/", true);
string learn_sounds_normalize_path(const string& in path) {
return path.replace("\\", "/", true);
}
bool learn_sounds_is_directory_skip_entry(const string&in entry) {
if (entry.length() == 0) return false;
if (entry.substr(entry.length() - 1) == "/") return true;
return directory_exists(entry);
bool learn_sounds_is_directory_skip_entry(const string& in entry) {
if (entry.length() == 0)
return false;
if (entry.substr(entry.length() - 1) == "/")
return true;
return directory_exists(entry);
}
bool learn_sounds_should_skip_path(const string&in path) {
string normalizedPath = learn_sounds_normalize_path(path);
for (uint skipIndex = 0; skipIndex < learnSoundsSkipList.length(); skipIndex++) {
string entry = learn_sounds_normalize_path(learnSoundsSkipList[skipIndex]);
if (entry.length() == 0) continue;
bool learn_sounds_should_skip_path(const string& in path) {
string normalizedPath = learn_sounds_normalize_path(path);
for (uint skipIndex = 0; skipIndex < learnSoundsSkipList.length(); skipIndex++) {
string entry = learn_sounds_normalize_path(learnSoundsSkipList[skipIndex]);
if (entry.length() == 0)
continue;
bool isDirectory = learn_sounds_is_directory_skip_entry(entry);
if (isDirectory) {
if (entry.substr(entry.length() - 1) != "/") entry += "/";
if (normalizedPath.length() >= entry.length() &&
normalizedPath.substr(0, entry.length()) == entry) {
return true;
}
continue;
}
bool isDirectory = learn_sounds_is_directory_skip_entry(entry);
if (isDirectory) {
if (entry.substr(entry.length() - 1) != "/")
entry += "/";
if (normalizedPath.length() >= entry.length() && normalizedPath.substr(0, entry.length()) == entry) {
return true;
}
continue;
}
if (normalizedPath == entry) return true;
if (normalizedPath == entry)
return true;
if (entry.find_first("/") < 0 && entry.find_first("\\") < 0) {
if (normalizedPath.length() >= entry.length() + 1 &&
normalizedPath.substr(normalizedPath.length() - entry.length()) == entry) {
return true;
}
}
}
return false;
if (entry.find_first("/") < 0 && entry.find_first("\\") < 0) {
if (normalizedPath.length() >= entry.length() + 1 &&
normalizedPath.substr(normalizedPath.length() - entry.length()) == entry) {
return true;
}
}
}
return false;
}
string learn_sounds_get_description(const string&in path) {
string normalizedPath = learn_sounds_normalize_path(path);
uint descriptionCount = learnSoundsDescriptionPaths.length();
if (learnSoundsDescriptionTexts.length() < descriptionCount) {
descriptionCount = learnSoundsDescriptionTexts.length();
}
string learn_sounds_get_description(const string& in path) {
string normalizedPath = learn_sounds_normalize_path(path);
uint descriptionCount = learnSoundsDescriptionPaths.length();
if (learnSoundsDescriptionTexts.length() < descriptionCount) {
descriptionCount = learnSoundsDescriptionTexts.length();
}
for (uint descriptionIndex = 0; descriptionIndex < descriptionCount; descriptionIndex++) {
if (learn_sounds_normalize_path(learnSoundsDescriptionPaths[descriptionIndex]) == normalizedPath) {
return learnSoundsDescriptionTexts[descriptionIndex];
}
}
for (uint descriptionIndex = 0; descriptionIndex < descriptionCount; descriptionIndex++) {
if (learn_sounds_normalize_path(learnSoundsDescriptionPaths[descriptionIndex]) == normalizedPath) {
return learnSoundsDescriptionTexts[descriptionIndex];
}
}
return "";
return "";
}
void learn_sounds_gather_files_recursive(const string&in basePath, string[]@ outFiles) {
string[]@ oggFiles = find_files(basePath + "/*.ogg");
if (@oggFiles !is null) {
for (uint fileIndex = 0; fileIndex < oggFiles.length(); fileIndex++) {
outFiles.insert_last(basePath + "/" + oggFiles[fileIndex]);
}
}
string[]@ oggUpperFiles = find_files(basePath + "/*.OGG");
if (@oggUpperFiles !is null) {
for (uint fileIndex = 0; fileIndex < oggUpperFiles.length(); fileIndex++) {
outFiles.insert_last(basePath + "/" + oggUpperFiles[fileIndex]);
}
}
string[]@ wavFiles = find_files(basePath + "/*.wav");
if (@wavFiles !is null) {
for (uint fileIndex = 0; fileIndex < wavFiles.length(); fileIndex++) {
outFiles.insert_last(basePath + "/" + wavFiles[fileIndex]);
}
}
string[]@ wavUpperFiles = find_files(basePath + "/*.WAV");
if (@wavUpperFiles !is null) {
for (uint fileIndex = 0; fileIndex < wavUpperFiles.length(); fileIndex++) {
outFiles.insert_last(basePath + "/" + wavUpperFiles[fileIndex]);
}
}
string[]@ folders = find_directories(basePath + "/*");
if (@folders !is null) {
for (uint folderIndex = 0; folderIndex < folders.length(); folderIndex++) {
learn_sounds_gather_files_recursive(basePath + "/" + folders[folderIndex], outFiles);
}
}
bool learn_sounds_contains_path_ci(string[] @paths, const string& in path) {
string normalizedPath = learn_sounds_normalize_path(path).lower();
for (uint pathIndex = 0; pathIndex < paths.length(); pathIndex++) {
if (learn_sounds_normalize_path(paths[pathIndex]).lower() == normalizedPath) {
return true;
}
}
return false;
}
bool learn_sounds_sort_case_insensitive(const string &in a, const string &in b) {
return a.lower() < b.lower();
void learn_sounds_add_path_if_missing(string[] @paths, const string& in path) {
if (learn_sounds_contains_path_ci(paths, path)) {
return;
}
paths.insert_last(path);
}
string learn_sounds_collapse_spaces(const string&in text) {
string result = text;
while (result.find_first(" ") > -1) {
result = result.replace(" ", " ", true);
}
return result;
bool learn_sounds_has_audio_extension(const string& in path) {
string lowerPath = path.lower();
if (lowerPath.length() < 4)
return false;
string suffix = lowerPath.substr(lowerPath.length() - 4);
return suffix == ".ogg" || suffix == ".wav";
}
string learn_sounds_label_from_path(const string&in soundPath) {
string normalizedPath = learn_sounds_normalize_path(soundPath);
int slashPos = normalizedPath.find_last_of("/");
string name = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath;
void learn_sounds_gather_files_from_pack(const string& in basePath, string[] @outFiles) {
pack @activePack = cast<pack @>(sound_default_pack);
if (@activePack is null)
return;
string lowerName = name.lower();
if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") {
name = name.substr(0, name.length() - 4);
} else if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".wav") {
name = name.substr(0, name.length() - 4);
}
string[] @packFiles = activePack.list_files();
if (@packFiles is null)
return;
name = name.replace("_", " ", true);
name = name.replace("-", " ", true);
name = learn_sounds_collapse_spaces(name);
name = name.lower();
name.trim_whitespace_this();
string normalizedBasePath = learn_sounds_normalize_path(basePath);
if (normalizedBasePath != "" && normalizedBasePath.substr(normalizedBasePath.length() - 1) != "/") {
normalizedBasePath += "/";
}
string normalizedBasePathLower = normalizedBasePath.lower();
if (name.length() == 0) return "Sound";
return name;
for (uint fileIndex = 0; fileIndex < packFiles.length(); fileIndex++) {
string normalizedPath = learn_sounds_normalize_path(packFiles[fileIndex]);
if (normalizedPath.find("./") == 0) {
normalizedPath = normalizedPath.substr(2);
}
if (!learn_sounds_has_audio_extension(normalizedPath)) {
continue;
}
if (normalizedBasePathLower != "") {
if (normalizedPath.length() < normalizedBasePathLower.length()) {
continue;
}
if (normalizedPath.substr(0, normalizedBasePathLower.length()).lower() != normalizedBasePathLower) {
continue;
}
}
learn_sounds_add_path_if_missing(outFiles, normalizedPath);
}
}
void learn_sounds_collect_entries(string[]@ labels, string[]@ soundPaths) {
labels.resize(0);
soundPaths.resize(0);
void learn_sounds_gather_files_recursive(const string& in basePath, string[] @outFiles) {
string[] @oggFiles = find_files(basePath + "/*.ogg");
if (@oggFiles !is null) {
for (uint fileIndex = 0; fileIndex < oggFiles.length(); fileIndex++) {
learn_sounds_add_path_if_missing(outFiles, basePath + "/" + oggFiles[fileIndex]);
}
}
if (!directory_exists(learnSoundsRootDir)) {
return;
}
string[] @oggUpperFiles = find_files(basePath + "/*.OGG");
if (@oggUpperFiles !is null) {
for (uint fileIndex = 0; fileIndex < oggUpperFiles.length(); fileIndex++) {
learn_sounds_add_path_if_missing(outFiles, basePath + "/" + oggUpperFiles[fileIndex]);
}
}
string[] discoveredFiles;
learn_sounds_gather_files_recursive(learnSoundsRootDir, discoveredFiles);
if (discoveredFiles.length() > 1) {
discoveredFiles.sort(learn_sounds_sort_case_insensitive);
}
string[] @wavFiles = find_files(basePath + "/*.wav");
if (@wavFiles !is null) {
for (uint fileIndex = 0; fileIndex < wavFiles.length(); fileIndex++) {
learn_sounds_add_path_if_missing(outFiles, basePath + "/" + wavFiles[fileIndex]);
}
}
for (uint fileIndex = 0; fileIndex < discoveredFiles.length(); fileIndex++) {
string soundPath = learn_sounds_normalize_path(discoveredFiles[fileIndex]);
if (learn_sounds_should_skip_path(soundPath)) {
continue;
}
string[] @wavUpperFiles = find_files(basePath + "/*.WAV");
if (@wavUpperFiles !is null) {
for (uint fileIndex = 0; fileIndex < wavUpperFiles.length(); fileIndex++) {
learn_sounds_add_path_if_missing(outFiles, basePath + "/" + wavUpperFiles[fileIndex]);
}
}
string label = learn_sounds_label_from_path(soundPath);
string description = learn_sounds_get_description(soundPath);
if (description.length() > 0) {
label += " - " + description;
}
labels.insert_last(label);
soundPaths.insert_last(soundPath);
}
string[] @folders = find_directories(basePath + "/*");
if (@folders !is null) {
for (uint folderIndex = 0; folderIndex < folders.length(); folderIndex++) {
learn_sounds_gather_files_recursive(basePath + "/" + folders[folderIndex], outFiles);
}
}
}
void learn_sounds_play_ui_sound(sound &inout soundObj, const string basePath) {
string soundPath = resolve_audio_path(basePath);
if (soundPath == "") {
return;
}
bool learn_sounds_sort_case_insensitive(const string& in a, const string& in b) {
return a.lower() < b.lower();
}
soundObj.close();
if (!soundObj.load(soundPath)) {
return;
}
soundObj.play();
string learn_sounds_collapse_spaces(const string& in text) {
string result = text;
while (result.find_first(" ") > -1) {
result = result.replace(" ", " ", true);
}
return result;
}
string learn_sounds_label_from_path(const string& in soundPath) {
string normalizedPath = learn_sounds_normalize_path(soundPath);
int slashPos = normalizedPath.find_last_of("/");
string name = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath;
string lowerName = name.lower();
if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") {
name = name.substr(0, name.length() - 4);
} else if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".wav") {
name = name.substr(0, name.length() - 4);
}
name = name.replace("_", " ", true);
name = name.replace("-", " ", true);
name = learn_sounds_collapse_spaces(name);
name = name.lower();
name.trim_whitespace_this();
if (name.length() == 0)
return "Sound";
return name;
}
void learn_sounds_collect_entries(string[] @labels, string[] @soundPaths) {
labels.resize(0);
soundPaths.resize(0);
string[] discoveredFiles;
if (directory_exists(learnSoundsRootDir)) {
learn_sounds_gather_files_recursive(learnSoundsRootDir, discoveredFiles);
}
learn_sounds_gather_files_from_pack(learnSoundsRootDir, discoveredFiles);
if (discoveredFiles.length() > 1) {
discoveredFiles.sort(learn_sounds_sort_case_insensitive);
}
for (uint fileIndex = 0; fileIndex < discoveredFiles.length(); fileIndex++) {
string soundPath = learn_sounds_normalize_path(discoveredFiles[fileIndex]);
if (learn_sounds_should_skip_path(soundPath)) {
continue;
}
string label = i18n_text(learn_sounds_label_from_path(soundPath));
string description = learn_sounds_get_description(soundPath);
if (description.length() > 0) {
label += " - " + description;
}
labels.insert_last(label);
soundPaths.insert_last(soundPath);
}
}
void learn_sounds_play_ui_sound(sound& inout soundObj, const string basePath) {
soundObj.close();
// Avoid file_exists()-only path checks so packaged Android assets can still load.
if (soundObj.load(basePath)) {
soundObj.play();
return;
}
if (soundObj.load(basePath + ".ogg")) {
soundObj.play();
return;
}
if (!soundObj.load(basePath + ".wav")) {
return;
}
soundObj.play();
}
void learn_sounds_run_menu() {
learn_sounds_apply_setup_once();
learn_sounds_apply_setup_once();
string[] labels;
string[] soundPaths;
learn_sounds_collect_entries(labels, soundPaths);
string[] labels;
string[] soundPaths;
learn_sounds_collect_entries(labels, soundPaths);
learn_sounds_speak("Learn sounds.", true);
if (labels.length() == 0) {
learn_sounds_speak("No sounds available.", true);
return;
}
learn_sounds_speak("Learn sounds.", true);
if (labels.length() == 0) {
learn_sounds_speak("No sounds available.", true);
return;
}
int selection = 0;
learn_sounds_speak(labels[selection], true);
int selection = 0;
learn_sounds_speak(labels[selection], true);
while (true) {
wait(5);
learn_sounds_tick();
while (true) {
wait(5);
learn_sounds_tick();
if (key_pressed(KEY_ESCAPE)) {
learn_sounds_speak("Closed.", true);
return;
}
if (key_pressed(KEY_ESCAPE)) {
learn_sounds_speak("Closed.", true);
return;
}
if (key_pressed(KEY_DOWN)) {
learn_sounds_play_ui_sound(learnSoundsMoveSound, learnSoundsMenuSoundDir + "/menu_move");
selection++;
if (selection >= int(labels.length())) {
selection = learnSoundsWrap ? 0 : int(labels.length()) - 1;
}
learn_sounds_speak(labels[selection], true);
}
if (key_pressed(KEY_DOWN)) {
learn_sounds_play_ui_sound(learnSoundsMoveSound, learnSoundsMenuSoundDir + "/menu_move");
selection++;
if (selection >= int(labels.length())) {
selection = learnSoundsWrap ? 0 : int(labels.length()) - 1;
}
learn_sounds_speak(labels[selection], true);
}
if (key_pressed(KEY_UP)) {
learn_sounds_play_ui_sound(learnSoundsMoveSound, learnSoundsMenuSoundDir + "/menu_move");
selection--;
if (selection < 0) {
selection = learnSoundsWrap ? int(labels.length()) - 1 : 0;
}
learn_sounds_speak(labels[selection], true);
}
if (key_pressed(KEY_UP)) {
learn_sounds_play_ui_sound(learnSoundsMoveSound, learnSoundsMenuSoundDir + "/menu_move");
selection--;
if (selection < 0) {
selection = learnSoundsWrap ? int(labels.length()) - 1 : 0;
}
learn_sounds_speak(labels[selection], true);
}
if (key_pressed(KEY_RETURN)) {
if (learnSoundsPlaySelectSound) {
learn_sounds_play_ui_sound(learnSoundsSelectSound, learnSoundsMenuSoundDir + "/menu_select");
}
string selectedPath = soundPaths[selection];
if (!file_exists(selectedPath)) {
learn_sounds_speak("Sound not found.", true);
continue;
}
if (key_pressed(KEY_RETURN)) {
if (learnSoundsPlaySelectSound) {
learn_sounds_play_ui_sound(learnSoundsSelectSound, learnSoundsMenuSoundDir + "/menu_select");
}
string selectedPath = soundPaths[selection];
learnSoundsPreviewSound.close();
if (!learnSoundsPreviewSound.load(selectedPath)) {
learn_sounds_speak("Unable to load sound.", true);
continue;
}
learnSoundsPreviewSound.play();
}
}
learnSoundsPreviewSound.close();
if (!learnSoundsPreviewSound.load(selectedPath)) {
learn_sounds_speak("Unable to load sound.", true);
continue;
}
learnSoundsPreviewSound.play();
}
}
}

View File

@@ -3,120 +3,150 @@
// Applies common menu sounds from a directory.
// Looks for both .ogg and .wav files automatically.
void menu_apply_default_sounds(menu@ menuRef, const string soundDir = "sounds/menu") {
if (@menuRef is null) {
return;
}
void menu_apply_default_sounds(menu @menuRef, const string soundDir = "sounds/menu") {
if (@menuRef is null) {
return;
}
menuRef.click_sound = resolve_audio_path(soundDir + "/menu_move");
menuRef.select_sound = resolve_audio_path(soundDir + "/menu_select");
menuRef.edge_sound = resolve_audio_path(soundDir + "/menu_edge");
menuRef.wrap_sound = resolve_audio_path(soundDir + "/menu_wrap");
menuRef.open_sound = resolve_audio_path(soundDir + "/menu_open");
menuRef.close_sound = resolve_audio_path(soundDir + "/menu_close");
menuRef.click_sound = resolve_audio_path(soundDir + "/menu_move");
menuRef.select_sound = resolve_audio_path(soundDir + "/menu_select");
menuRef.edge_sound = resolve_audio_path(soundDir + "/menu_edge");
menuRef.wrap_sound = resolve_audio_path(soundDir + "/menu_wrap");
menuRef.open_sound = resolve_audio_path(soundDir + "/menu_open");
menuRef.close_sound = resolve_audio_path(soundDir + "/menu_close");
}
// Minimal blocking list menu.
// Returns selected index or -1 on empty input/escape.
int menu_run_simple(const string introText, string[]@ options, bool wrap = true, int startIndex = 0, const string soundDir = "sounds/menu") {
if (@options is null || options.length() == 0) {
return -1;
}
int menu_run_simple(const string introText, string[] @options, bool wrap = true, int startIndex = 0,
const string soundDir = "sounds/menu") {
if (@options is null || options.length() == 0) {
return -1;
}
menu menuRef;
menu_apply_default_sounds(menuRef, soundDir);
menuRef.intro_text = introText;
menuRef.wrap = wrap;
menuRef.focus_first_item = true;
menuRef.add_items(options);
menu menuRef;
menu_apply_default_sounds(menuRef, soundDir);
menuRef.intro_text = introText;
menuRef.wrap = wrap;
menuRef.focus_first_item = true;
menuRef.add_items(options);
if (startIndex >= 0 && startIndex < int(options.length())) {
menuRef.focused_item = startIndex;
}
if (startIndex >= 0 && startIndex < int(options.length())) {
menuRef.focused_item = startIndex;
}
return menuRef.run();
return menuRef.run();
}
// Returns a-z for menu prefix filtering, or empty string when no letter key was pressed.
string menu_get_filter_letter() {
if (key_pressed(KEY_A)) return "a";
if (key_pressed(KEY_B)) return "b";
if (key_pressed(KEY_C)) return "c";
if (key_pressed(KEY_D)) return "d";
if (key_pressed(KEY_E)) return "e";
if (key_pressed(KEY_F)) return "f";
if (key_pressed(KEY_G)) return "g";
if (key_pressed(KEY_H)) return "h";
if (key_pressed(KEY_I)) return "i";
if (key_pressed(KEY_J)) return "j";
if (key_pressed(KEY_K)) return "k";
if (key_pressed(KEY_L)) return "l";
if (key_pressed(KEY_M)) return "m";
if (key_pressed(KEY_N)) return "n";
if (key_pressed(KEY_O)) return "o";
if (key_pressed(KEY_P)) return "p";
if (key_pressed(KEY_Q)) return "q";
if (key_pressed(KEY_R)) return "r";
if (key_pressed(KEY_S)) return "s";
if (key_pressed(KEY_T)) return "t";
if (key_pressed(KEY_U)) return "u";
if (key_pressed(KEY_V)) return "v";
if (key_pressed(KEY_W)) return "w";
if (key_pressed(KEY_X)) return "x";
if (key_pressed(KEY_Y)) return "y";
if (key_pressed(KEY_Z)) return "z";
return "";
if (key_pressed(KEY_A))
return "a";
if (key_pressed(KEY_B))
return "b";
if (key_pressed(KEY_C))
return "c";
if (key_pressed(KEY_D))
return "d";
if (key_pressed(KEY_E))
return "e";
if (key_pressed(KEY_F))
return "f";
if (key_pressed(KEY_G))
return "g";
if (key_pressed(KEY_H))
return "h";
if (key_pressed(KEY_I))
return "i";
if (key_pressed(KEY_J))
return "j";
if (key_pressed(KEY_K))
return "k";
if (key_pressed(KEY_L))
return "l";
if (key_pressed(KEY_M))
return "m";
if (key_pressed(KEY_N))
return "n";
if (key_pressed(KEY_O))
return "o";
if (key_pressed(KEY_P))
return "p";
if (key_pressed(KEY_Q))
return "q";
if (key_pressed(KEY_R))
return "r";
if (key_pressed(KEY_S))
return "s";
if (key_pressed(KEY_T))
return "t";
if (key_pressed(KEY_U))
return "u";
if (key_pressed(KEY_V))
return "v";
if (key_pressed(KEY_W))
return "w";
if (key_pressed(KEY_X))
return "x";
if (key_pressed(KEY_Y))
return "y";
if (key_pressed(KEY_Z))
return "z";
return "";
}
// Applies a prefix filter to menu options.
void menu_apply_prefix_filter(const string &in filterText, const string[]@ options, int[]@ filteredIndices, string[]@ filteredOptions) {
filteredIndices.resize(0);
filteredOptions.resize(0);
void menu_apply_prefix_filter(const string& in filterText, const string[] @options, int[] @filteredIndices,
string[] @filteredOptions) {
filteredIndices.resize(0);
filteredOptions.resize(0);
if (@options is null) {
return;
}
if (@options is null) {
return;
}
string filterLower = filterText.lower();
for (uint optionIndex = 0; optionIndex < options.length(); optionIndex++) {
if (filterLower.length() == 0) {
filteredIndices.insert_last(optionIndex);
filteredOptions.insert_last(options[optionIndex]);
continue;
}
string filterLower = filterText.lower();
for (uint optionIndex = 0; optionIndex < options.length(); optionIndex++) {
if (filterLower.length() == 0) {
filteredIndices.insert_last(optionIndex);
filteredOptions.insert_last(options[optionIndex]);
continue;
}
string optionLower = options[optionIndex].lower();
if (optionLower.length() >= filterLower.length() && optionLower.substr(0, filterLower.length()) == filterLower) {
filteredIndices.insert_last(optionIndex);
filteredOptions.insert_last(options[optionIndex]);
}
}
string optionLower = options[optionIndex].lower();
if (optionLower.length() >= filterLower.length() &&
optionLower.substr(0, filterLower.length()) == filterLower) {
filteredIndices.insert_last(optionIndex);
filteredOptions.insert_last(options[optionIndex]);
}
}
}
// Updates filter text from keyboard input and reapplies filtering.
bool menu_update_prefix_filter(string &inout filterText, const string[]@ options, int[]@ filteredIndices, string[]@ filteredOptions, int &inout selection) {
bool filterChanged = false;
bool menu_update_prefix_filter(string& inout filterText, const string[] @options, int[] @filteredIndices,
string[] @filteredOptions, int& inout selection) {
bool filterChanged = false;
if (key_pressed(KEY_BACK) && filterText.length() > 0) {
filterText = filterText.substr(0, filterText.length() - 1);
filterChanged = true;
}
if (key_pressed(KEY_BACK) && filterText.length() > 0) {
filterText = filterText.substr(0, filterText.length() - 1);
filterChanged = true;
}
string filterLetter = menu_get_filter_letter();
if (filterLetter != "") {
filterText += filterLetter;
filterChanged = true;
}
string filterLetter = menu_get_filter_letter();
if (filterLetter != "") {
filterText += filterLetter;
filterChanged = true;
}
if (filterChanged) {
menu_apply_prefix_filter(filterText, options, filteredIndices, filteredOptions);
if (selection < 0) {
selection = 0;
}
if (selection >= int(filteredOptions.length())) {
selection = 0;
}
}
if (filterChanged) {
menu_apply_prefix_filter(filterText, options, filteredIndices, filteredOptions);
if (selection < 0) {
selection = 0;
}
if (selection >= int(filteredOptions.length())) {
selection = 0;
}
}
return filterChanged;
return filterChanged;
}

View File

@@ -1,30 +1,30 @@
#include "music.nvgt"
// Resume paused menu music when possible, otherwise start a new track.
void menu_music_resume_or_play(music_manager &inout manager, const string track, const int fadeInMs = 0) {
if (manager.resume(fadeInMs)) {
return;
}
void menu_music_resume_or_play(music_manager& inout manager, const string track, const int fadeInMs = 0) {
if (manager.resume(fadeInMs)) {
return;
}
if (manager.playing) {
// Normalize state before fading back up.
manager.pause(0, true);
manager.resume(fadeInMs);
return;
}
if (manager.playing) {
// Normalize state before fading back up.
manager.pause(0, true);
manager.resume(fadeInMs);
return;
}
manager.play(track);
manager.play(track);
}
// Pause menu music. Blocking is enabled by default so fade-out fully completes.
bool menu_music_pause(music_manager &inout manager, const int fadeOutMs = 0, const bool blocking = true) {
if (!manager.playing) {
return false;
}
return manager.pause(fadeOutMs, blocking);
bool menu_music_pause(music_manager& inout manager, const int fadeOutMs = 0, const bool blocking = true) {
if (!manager.playing) {
return false;
}
return manager.pause(fadeOutMs, blocking);
}
// Stop menu music and reset playback state.
void menu_music_stop(music_manager &inout manager, const int fadeOutMs = 0, const bool blocking = false) {
manager.stop(fadeOutMs, blocking);
void menu_music_stop(music_manager& inout manager, const int fadeOutMs = 0, const bool blocking = false) {
manager.stop(fadeOutMs, blocking);
}

View File

@@ -1,5 +1,5 @@
// Multikey input helpers.
bool check_key_down(array<int>@ keys) {
bool check_key_down(array<int> @keys) {
// True when at least one key in the set is currently down.
if (keys is null || keys.length() == 0) {
return false;
@@ -14,7 +14,7 @@ bool check_key_down(array<int>@ keys) {
return false;
}
bool check_all_keys(array<int>@ keys) {
bool check_all_keys(array<int> @keys) {
// True only when every key in the set is currently down.
if (keys is null || keys.length() == 0) {
return false;

View File

@@ -1,101 +1,120 @@
// Name and filename sanitizing helpers.
string normalize_name_whitespace(string name) {
string result = "";
bool lastWasSpace = true;
string result = "";
bool lastWasSpace = true;
for (uint charIndex = 0; charIndex < name.length(); charIndex++) {
string ch = name.substr(charIndex, 1);
bool isSpace = (ch == " " || ch == "\t" || ch == "\r" || ch == "\n");
if (isSpace) {
if (!lastWasSpace) {
result += " ";
lastWasSpace = true;
}
continue;
}
for (uint charIndex = 0; charIndex < name.length(); charIndex++) {
string ch = name.substr(charIndex, 1);
bool isSpace = (ch == " " || ch == "\t" || ch == "\r" || ch == "\n");
if (isSpace) {
if (!lastWasSpace) {
result += " ";
lastWasSpace = true;
}
continue;
}
result += ch;
lastWasSpace = false;
}
result += ch;
lastWasSpace = false;
}
if (result.length() > 0 && result.substr(result.length() - 1) == " ") {
result = result.substr(0, result.length() - 1);
}
if (result.length() > 0 && result.substr(result.length() - 1) == " ") {
result = result.substr(0, result.length() - 1);
}
return result;
return result;
}
bool is_windows_reserved_filename(const string&in upperName) {
if (upperName == "CON" || upperName == "PRN" || upperName == "AUX" || upperName == "NUL") return true;
bool is_windows_reserved_filename(const string& in upperName) {
if (upperName == "CON" || upperName == "PRN" || upperName == "AUX" || upperName == "NUL")
return true;
if (upperName.length() == 4 && upperName.substr(0, 3) == "COM") {
int num = parse_int(upperName.substr(3));
if (num >= 1 && num <= 9) return true;
}
if (upperName.length() == 4 && upperName.substr(0, 3) == "COM") {
int num = parse_int(upperName.substr(3));
if (num >= 1 && num <= 9)
return true;
}
if (upperName.length() == 4 && upperName.substr(0, 3) == "LPT") {
int num = parse_int(upperName.substr(3));
if (num >= 1 && num <= 9) return true;
}
if (upperName.length() == 4 && upperName.substr(0, 3) == "LPT") {
int num = parse_int(upperName.substr(3));
if (num >= 1 && num <= 9)
return true;
}
return false;
return false;
}
string sanitize_filename_base_with_reserved_prefix(string name, const int maxLength = 40,
const string fallback = "item",
const string reservedPrefix = "file_") {
string normalized = normalize_name_whitespace(name);
string result = "";
bool lastSeparator = false;
for (uint charIndex = 0; charIndex < normalized.length(); charIndex++) {
string ch = normalized.substr(charIndex, 1);
bool isUpper = (ch >= "A" && ch <= "Z");
bool isLower = (ch >= "a" && ch <= "z");
bool isDigit = (ch >= "0" && ch <= "9");
if (isUpper || isLower || isDigit) {
result += ch;
lastSeparator = false;
continue;
}
if (ch == " " || ch == "_" || ch == "-") {
if (!lastSeparator && result.length() > 0) {
result += "_";
lastSeparator = true;
}
}
}
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
result = result.substr(0, result.length() - 1);
}
if (result.length() == 0) {
result = fallback;
}
if (result.length() > maxLength) {
result = result.substr(0, maxLength);
}
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
result = result.substr(0, result.length() - 1);
}
string upperName = result.upper();
if (upperName == "." || upperName == "..") {
result = fallback;
upperName = result.upper();
}
if (is_windows_reserved_filename(upperName)) {
string safePrefix = reservedPrefix;
if (safePrefix == "")
safePrefix = "file_";
result = safePrefix + result;
}
return result;
}
string sanitize_filename_base(string name, const int maxLength = 40, const string fallback = "item") {
string normalized = normalize_name_whitespace(name);
string result = "";
bool lastSeparator = false;
for (uint charIndex = 0; charIndex < normalized.length(); charIndex++) {
string ch = normalized.substr(charIndex, 1);
bool isUpper = (ch >= "A" && ch <= "Z");
bool isLower = (ch >= "a" && ch <= "z");
bool isDigit = (ch >= "0" && ch <= "9");
if (isUpper || isLower || isDigit) {
result += ch;
lastSeparator = false;
continue;
}
if (ch == " " || ch == "_" || ch == "-") {
if (!lastSeparator && result.length() > 0) {
result += "_";
lastSeparator = true;
}
}
}
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
result = result.substr(0, result.length() - 1);
}
if (result.length() == 0) {
result = fallback;
}
if (result.length() > maxLength) {
result = result.substr(0, maxLength);
}
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
result = result.substr(0, result.length() - 1);
}
string upperName = result.upper();
if (upperName == "." || upperName == "..") {
result = fallback;
upperName = result.upper();
}
if (is_windows_reserved_filename(upperName)) {
result = "file_" + result;
}
return result;
return sanitize_filename_base_with_reserved_prefix(name, maxLength, fallback, "file_");
}
string build_filename_from_name(const string&in name, const string&in extension = ".dat", const int maxBaseLength = 40, const string fallback = "item") {
return sanitize_filename_base(name, maxBaseLength, fallback) + extension;
string build_filename_from_name_ex(const string& in name, const string& in extension = ".dat",
const int maxBaseLength = 40, const string fallback = "item",
const string reservedPrefix = "file_") {
return sanitize_filename_base_with_reserved_prefix(name, maxBaseLength, fallback, reservedPrefix) + extension;
}
string build_filename_from_name(const string& in name, const string& in extension = ".dat",
const int maxBaseLength = 40, const string fallback = "item") {
return build_filename_from_name_ex(name, extension, maxBaseLength, fallback, "file_");
}

View File

@@ -1,6 +1,6 @@
#include "audio_paths.nvgt"
funcdef void notification_speak_callback(const string &in message, bool interrupt);
funcdef void notification_speak_callback(const string& in message, bool interrupt);
string[] notificationsHistory;
string[] notificationsQueue;
@@ -13,163 +13,176 @@ timer notificationsDelayTimer;
sound notificationsSound;
bool notificationsWaitingForSound = false;
string notificationsSoundPath = "sounds/notify";
notification_speak_callback@ notificationsSpeakCallback = null;
notification_speak_callback @notificationsSpeakCallback = null;
void notifications_set_speak_callback(notification_speak_callback@ callback) {
@notificationsSpeakCallback = @callback;
void notifications_set_speak_callback(notification_speak_callback @callback) {
@notificationsSpeakCallback = @callback;
}
void notifications_set_sound_path(const string soundPath) {
notificationsSoundPath = soundPath;
notificationsSoundPath = soundPath;
}
void notifications_set_delay_ms(int delayMs) {
if (delayMs < 0) {
delayMs = 0;
}
notificationsDelayMs = delayMs;
if (delayMs < 0) {
delayMs = 0;
}
notificationsDelayMs = delayMs;
}
void notifications_set_max_history(int maxCount) {
if (maxCount < 1) {
maxCount = 1;
}
notificationsMaxHistory = maxCount;
if (maxCount < 1) {
maxCount = 1;
}
notificationsMaxHistory = maxCount;
while (notificationsHistory.length() > uint(notificationsMaxHistory)) {
notificationsHistory.remove_at(0);
}
while (notificationsHistory.length() > uint(notificationsMaxHistory)) {
notificationsHistory.remove_at(0);
}
if (notificationsHistory.length() == 0) {
notificationsCurrentIndex = -1;
} else if (notificationsCurrentIndex >= int(notificationsHistory.length())) {
notificationsCurrentIndex = notificationsHistory.length() - 1;
}
if (notificationsHistory.length() == 0) {
notificationsCurrentIndex = -1;
} else if (notificationsCurrentIndex >= int(notificationsHistory.length())) {
notificationsCurrentIndex = notificationsHistory.length() - 1;
}
}
void notifications_clear() {
notificationsHistory.resize(0);
notificationsQueue.resize(0);
notificationsCurrentIndex = -1;
notificationsActive = false;
notificationsWaitingForSound = false;
notificationsSound.close();
notificationsHistory.resize(0);
notificationsQueue.resize(0);
notificationsCurrentIndex = -1;
notificationsActive = false;
notificationsWaitingForSound = false;
notificationsSound.close();
}
void notifications_speak(const string &in message, bool interrupt) {
if (@notificationsSpeakCallback !is null) {
notificationsSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
void notifications_speak(const string& in message, bool interrupt) {
if (@notificationsSpeakCallback !is null) {
notificationsSpeakCallback(message, interrupt);
return;
}
screen_reader_speak(message, interrupt);
}
void notifications_enqueue(const string &in message) {
notificationsQueue.insert_last(message);
notificationsHistory.insert_last(message);
void notifications_enqueue(const string& in message) {
notificationsQueue.insert_last(message);
notificationsHistory.insert_last(message);
while (notificationsHistory.length() > uint(notificationsMaxHistory)) {
notificationsHistory.remove_at(0);
}
while (notificationsHistory.length() > uint(notificationsMaxHistory)) {
notificationsHistory.remove_at(0);
}
notificationsCurrentIndex = notificationsHistory.length() - 1;
notificationsCurrentIndex = notificationsHistory.length() - 1;
}
bool notifications_try_play_sound() {
string resolvedPath = resolve_audio_path(notificationsSoundPath);
if (resolvedPath == "") {
return false;
}
notificationsSound.close();
notificationsSound.close();
if (!notificationsSound.load(resolvedPath)) {
return false;
}
// Avoid file_exists()-only checks so packaged Android assets can still load.
if (notificationsSound.load(notificationsSoundPath)) {
notificationsSound.play();
return true;
}
notificationsSound.play();
return true;
string oggPath = notificationsSoundPath + ".ogg";
if (notificationsSound.load(oggPath)) {
notificationsSound.play();
return true;
}
string wavPath = notificationsSoundPath + ".wav";
if (!notificationsSound.load(wavPath)) {
return false;
}
notificationsSound.play();
return true;
}
void notifications_update() {
if (notificationsQueue.length() == 0) {
if (notificationsActive && notificationsDelayTimer.elapsed >= notificationsDelayMs) {
notificationsActive = false;
}
return;
}
if (notificationsQueue.length() == 0) {
if (notificationsActive && notificationsDelayTimer.elapsed >= notificationsDelayMs) {
notificationsActive = false;
}
return;
}
if (notificationsActive && notificationsDelayTimer.elapsed < notificationsDelayMs) {
return;
}
if (notificationsActive && notificationsDelayTimer.elapsed < notificationsDelayMs) {
return;
}
if (notificationsWaitingForSound) {
if (notificationsSound.playing) {
return;
}
if (notificationsWaitingForSound) {
if (notificationsSound.playing) {
return;
}
notifications_speak(notificationsQueue[0], true);
notificationsQueue.remove_at(0);
notificationsWaitingForSound = false;
notificationsActive = true;
notificationsDelayTimer.restart();
return;
}
notifications_speak(notificationsQueue[0], true);
notificationsQueue.remove_at(0);
notificationsWaitingForSound = false;
notificationsActive = true;
notificationsDelayTimer.restart();
return;
}
if (notifications_try_play_sound()) {
notificationsWaitingForSound = true;
return;
}
if (notifications_try_play_sound()) {
notificationsWaitingForSound = true;
return;
}
notifications_speak(notificationsQueue[0], true);
notificationsQueue.remove_at(0);
notificationsActive = true;
notificationsDelayTimer.restart();
notifications_speak(notificationsQueue[0], true);
notificationsQueue.remove_at(0);
notificationsActive = true;
notificationsDelayTimer.restart();
}
void notifications_check_keys() {
if (key_pressed(KEY_LEFTBRACKET)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
if (key_pressed(KEY_LEFTBRACKET)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
notificationsCurrentIndex--;
if (notificationsCurrentIndex < 0) {
notificationsCurrentIndex = 0;
notifications_speak("Oldest notification. " + notificationsHistory[notificationsCurrentIndex], true);
return;
}
notificationsCurrentIndex--;
if (notificationsCurrentIndex < 0) {
notificationsCurrentIndex = 0;
notifications_speak("Oldest notification. " + notificationsHistory[notificationsCurrentIndex], true);
return;
}
int position = notificationsCurrentIndex + 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex] + " " + position + " of " + notificationsHistory.length(), true);
return;
}
int position = notificationsCurrentIndex + 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex] + " " + position + " of " +
notificationsHistory.length(),
true);
return;
}
if (key_pressed(KEY_RIGHTBRACKET)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
if (key_pressed(KEY_RIGHTBRACKET)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
notificationsCurrentIndex++;
if (notificationsCurrentIndex >= int(notificationsHistory.length())) {
notificationsCurrentIndex = notificationsHistory.length() - 1;
notifications_speak("Newest notification. " + notificationsHistory[notificationsCurrentIndex], true);
return;
}
notificationsCurrentIndex++;
if (notificationsCurrentIndex >= int(notificationsHistory.length())) {
notificationsCurrentIndex = notificationsHistory.length() - 1;
notifications_speak("Newest notification. " + notificationsHistory[notificationsCurrentIndex], true);
return;
}
int position = notificationsCurrentIndex + 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex] + " " + position + " of " + notificationsHistory.length(), true);
return;
}
int position = notificationsCurrentIndex + 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex] + " " + position + " of " +
notificationsHistory.length(),
true);
return;
}
if (key_pressed(KEY_BACKSLASH)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
if (key_pressed(KEY_BACKSLASH)) {
if (notificationsHistory.length() == 0) {
notifications_speak("No notifications.", true);
return;
}
notificationsCurrentIndex = notificationsHistory.length() - 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex], true);
}
notificationsCurrentIndex = notificationsHistory.length() - 1;
notifications_speak(notificationsHistory[notificationsCurrentIndex], true);
}
}

View File

@@ -1,101 +1,102 @@
// Generic save/file helpers.
bool save_utils_sort_string_case_insensitive(const string &in a, const string &in b) {
return a.lower() < b.lower();
bool save_utils_sort_string_case_insensitive(const string& in a, const string& in b) {
return a.lower() < b.lower();
}
string[] list_files_with_extension(const string&in extension, const string&in directory = "") {
string[] result;
if (extension == "") return result;
string[] list_files_with_extension(const string& in extension, const string& in directory = "") {
string[] result;
if (extension == "")
return result;
string dirPrefix = directory;
if (dirPrefix != "" && dirPrefix.substr(dirPrefix.length() - 1) != "/") {
dirPrefix += "/";
}
string dirPrefix = directory;
if (dirPrefix != "" && dirPrefix.substr(dirPrefix.length() - 1) != "/") {
dirPrefix += "/";
}
string[]@ items = glob(dirPrefix + "*" + extension);
if (@items is null) return result;
string[] @items = glob(dirPrefix + "*" + extension);
if (@items is null)
return result;
for (uint itemIndex = 0; itemIndex < items.length(); itemIndex++) {
string item = items[itemIndex];
if (item.length() >= extension.length() &&
item.substr(item.length() - extension.length()) == extension) {
result.insert_last(item);
}
}
for (uint itemIndex = 0; itemIndex < items.length(); itemIndex++) {
string item = items[itemIndex];
if (item.length() >= extension.length() && item.substr(item.length() - extension.length()) == extension) {
result.insert_last(item);
}
}
if (result.length() > 1) {
result.sort(save_utils_sort_string_case_insensitive);
}
return result;
if (result.length() > 1) {
result.sort(save_utils_sort_string_case_insensitive);
}
return result;
}
bool has_files_with_extension(const string&in extension, const string&in directory = "") {
return list_files_with_extension(extension, directory).length() > 0;
bool has_files_with_extension(const string& in extension, const string& in directory = "") {
return list_files_with_extension(extension, directory).length() > 0;
}
string strip_file_extension(const string&in filename, const string&in extension) {
if (extension != "" &&
filename.length() >= extension.length() &&
filename.substr(filename.length() - extension.length()) == extension) {
return filename.substr(0, filename.length() - extension.length());
}
return filename;
string strip_file_extension(const string& in filename, const string& in extension) {
if (extension != "" && filename.length() >= extension.length() &&
filename.substr(filename.length() - extension.length()) == extension) {
return filename.substr(0, filename.length() - extension.length());
}
return filename;
}
bool save_string_file(const string&in filename, const string&in data) {
if (data.length() == 0) {
return false;
}
bool save_string_file(const string& in filename, const string& in data) {
if (data.length() == 0) {
return false;
}
file outFile;
if (!outFile.open(filename, "wb")) {
return false;
}
file outFile;
if (!outFile.open(filename, "wb")) {
return false;
}
if (outFile.write(data) < data.length()) {
outFile.close();
return false;
}
if (outFile.write(data) < data.length()) {
outFile.close();
return false;
}
outFile.close();
return true;
outFile.close();
return true;
}
bool read_string_file(const string&in filename, string&out data, const bool allowEmpty = false) {
file inFile;
if (!inFile.open(filename, "rb")) {
return false;
}
bool read_string_file(const string& in filename, string& out data, const bool allowEmpty = false) {
file inFile;
if (!inFile.open(filename, "rb")) {
return false;
}
data = inFile.read();
inFile.close();
return allowEmpty || data.length() > 0;
data = inFile.read();
inFile.close();
return allowEmpty || data.length() > 0;
}
string encrypt_string_aes(const string&in rawData, const string&in key) {
return string_aes_encrypt(rawData, key);
string encrypt_string_aes(const string& in rawData, const string& in key) {
return string_aes_encrypt(rawData, key);
}
string decrypt_string_aes(const string&in encryptedData, const string&in key) {
return string_aes_decrypt(encryptedData, key);
string decrypt_string_aes(const string& in encryptedData, const string& in key) {
return string_aes_decrypt(encryptedData, key);
}
bool save_encrypted_file(const string&in filename, const string&in rawData, const string&in key) {
string encryptedData = encrypt_string_aes(rawData, key);
return save_string_file(filename, encryptedData);
bool save_encrypted_file(const string& in filename, const string& in rawData, const string& in key) {
string encryptedData = encrypt_string_aes(rawData, key);
return save_string_file(filename, encryptedData);
}
bool read_encrypted_file(const string&in filename, const string&in key, string&out rawData, const bool allowPlaintextFallback = true) {
string encryptedData = "";
if (!read_string_file(filename, encryptedData, false)) {
return false;
}
bool read_encrypted_file(const string& in filename, const string& in key, string& out rawData,
const bool allowPlaintextFallback = true) {
string encryptedData = "";
if (!read_string_file(filename, encryptedData, false)) {
return false;
}
rawData = decrypt_string_aes(encryptedData, key);
if (rawData.length() == 0 && allowPlaintextFallback) {
rawData = encryptedData;
}
rawData = decrypt_string_aes(encryptedData, key);
if (rawData.length() == 0 && allowPlaintextFallback) {
rawData = encryptedData;
}
return rawData.length() > 0;
return rawData.length() > 0;
}

76
scripts/format-nvgt.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
# Show simple usage help.
print_usage() {
echo "Usage: scripts/format-nvgt.sh [path/to/file.nvgt ...]"
echo "Formats all tracked .nvgt files when no file paths are provided."
}
scriptDir=""
repoRoot=""
filePath=""
formattedCount=0
targetFiles=()
# Help flag is optional and exits early without formatting.
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
print_usage
exit 0
fi
# Stop immediately if clang-format is not installed.
if ! command -v clang-format >/dev/null 2>&1; then
echo "clang-format is required but was not found in PATH." >&2
exit 1
fi
# Resolve script location, then run from repo root so relative paths work.
scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repoRoot="$(cd "${scriptDir}/.." && pwd)"
cd "${repoRoot}"
# We require the project style file to keep formatting consistent.
if [[ ! -f ".clang-format" ]]; then
echo "Missing .clang-format in repo root: ${repoRoot}" >&2
exit 1
fi
# No args: format every tracked .nvgt file in git.
if [[ "$#" -eq 0 ]]; then
mapfile -t targetFiles < <(git ls-files "*.nvgt")
else
# Args provided: validate each file and format only those paths.
for filePath in "$@"; do
if [[ ! -f "${filePath}" ]]; then
echo "File not found: ${filePath}" >&2
exit 1
fi
if [[ "${filePath}" != *.nvgt ]]; then
echo "Only .nvgt files are supported: ${filePath}" >&2
exit 1
fi
targetFiles+=("${filePath}")
done
fi
if [[ "${#targetFiles[@]}" -eq 0 ]]; then
echo "No .nvgt files found to format."
exit 0
fi
# Force C++ parsing rules for NVGT while still using repo .clang-format.
for filePath in "${targetFiles[@]}"; do
clang-format -i --style=file --assume-filename=file.cpp "${filePath}"
formattedCount=$((formattedCount + 1))
done
echo -n "Formatted ${formattedCount} "
if [[ ${formattedCount} -ne 1 ]]; then
echo "files."
else
echo "file."
fi
exit 0

View File

@@ -5,102 +5,121 @@ string[] speechHistory;
int speechHistoryMaxEntries = 10;
int speechHistoryCurrentIndex = -1;
bool speechHistoryDeduplicate = true;
funcdef string speech_history_message_transform_callback(const string& in message);
speech_history_message_transform_callback @speechHistoryMessageTransform = null;
void speech_history_set_message_transform_callback(speech_history_message_transform_callback @callback) {
@speechHistoryMessageTransform = @callback;
}
string speech_history_transform_message(const string& in message) {
if (@speechHistoryMessageTransform is null)
return message;
return speechHistoryMessageTransform(message);
}
void speech_history_set_max_entries(int maxEntries) {
if (maxEntries < 1) {
maxEntries = 1;
}
if (maxEntries < 1) {
maxEntries = 1;
}
speechHistoryMaxEntries = maxEntries;
while (speechHistory.length() > uint(speechHistoryMaxEntries)) {
speechHistory.remove_at(0);
}
speechHistoryMaxEntries = maxEntries;
while (speechHistory.length() > uint(speechHistoryMaxEntries)) {
speechHistory.remove_at(0);
}
if (speechHistory.length() == 0) {
speechHistoryCurrentIndex = -1;
} else if (speechHistoryCurrentIndex >= int(speechHistory.length())) {
speechHistoryCurrentIndex = speechHistory.length() - 1;
}
if (speechHistory.length() == 0) {
speechHistoryCurrentIndex = -1;
} else if (speechHistoryCurrentIndex >= int(speechHistory.length())) {
speechHistoryCurrentIndex = speechHistory.length() - 1;
}
}
void speech_history_clear() {
speechHistory.resize(0);
speechHistoryCurrentIndex = -1;
speechHistory.resize(0);
speechHistoryCurrentIndex = -1;
}
void speech_history_set_deduplicate(bool deduplicate) {
speechHistoryDeduplicate = deduplicate;
speechHistoryDeduplicate = deduplicate;
}
void speech_history_add(const string &in message) {
if (message == "") {
return;
}
void speech_history_add(const string& in message) {
if (message == "") {
return;
}
if (speechHistoryDeduplicate) {
for (uint messageIndex = 0; messageIndex < speechHistory.length(); messageIndex++) {
if (speechHistory[messageIndex] == message) {
speechHistoryCurrentIndex = messageIndex;
return;
}
}
}
if (speechHistoryDeduplicate) {
for (uint messageIndex = 0; messageIndex < speechHistory.length(); messageIndex++) {
if (speechHistory[messageIndex] == message) {
speechHistoryCurrentIndex = messageIndex;
return;
}
}
}
speechHistory.insert_last(message);
while (speechHistory.length() > uint(speechHistoryMaxEntries)) {
speechHistory.remove_at(0);
}
speechHistory.insert_last(message);
while (speechHistory.length() > uint(speechHistoryMaxEntries)) {
speechHistory.remove_at(0);
}
speechHistoryCurrentIndex = speechHistory.length() - 1;
speechHistoryCurrentIndex = speechHistory.length() - 1;
}
void speak_with_history(const string &in message, bool interrupt) {
speech_history_add(message);
screen_reader_speak(message, interrupt);
void speak_with_history(const string& in message, bool interrupt) {
string transformedMessage = speech_history_transform_message(message);
speech_history_add(transformedMessage);
screen_reader_speak(transformedMessage, interrupt);
}
void check_speech_history_keys() {
// Comma: older.
if (key_pressed(KEY_COMMA)) {
if (speechHistory.length() == 0) {
screen_reader_speak("No speech history.", true);
return;
}
// Comma: older.
if (key_pressed(KEY_COMMA)) {
if (speechHistory.length() == 0) {
screen_reader_speak(speech_history_transform_message("No speech history."), true);
return;
}
speechHistoryCurrentIndex--;
if (speechHistoryCurrentIndex < 0) {
speechHistoryCurrentIndex = 0;
screen_reader_speak("Oldest message. " + speechHistory[speechHistoryCurrentIndex], true);
return;
}
speechHistoryCurrentIndex--;
if (speechHistoryCurrentIndex < 0) {
speechHistoryCurrentIndex = 0;
screen_reader_speak(
speech_history_transform_message("Oldest message. " + speechHistory[speechHistoryCurrentIndex]), true);
return;
}
int position = speechHistoryCurrentIndex + 1;
screen_reader_speak(speechHistory[speechHistoryCurrentIndex] + " " + position + " of " + speechHistory.length(), true);
return;
}
int position = speechHistoryCurrentIndex + 1;
screen_reader_speak(speech_history_transform_message(speechHistory[speechHistoryCurrentIndex] + " " + position +
" of " + speechHistory.length()),
true);
return;
}
// Period: newer.
if (key_pressed(KEY_PERIOD)) {
if (speechHistory.length() == 0) {
screen_reader_speak("No speech history.", true);
return;
}
// Period: newer.
if (key_pressed(KEY_PERIOD)) {
if (speechHistory.length() == 0) {
screen_reader_speak(speech_history_transform_message("No speech history."), true);
return;
}
speechHistoryCurrentIndex++;
if (speechHistoryCurrentIndex >= int(speechHistory.length())) {
speechHistoryCurrentIndex = speechHistory.length() - 1;
screen_reader_speak("Newest message. " + speechHistory[speechHistoryCurrentIndex], true);
return;
}
speechHistoryCurrentIndex++;
if (speechHistoryCurrentIndex >= int(speechHistory.length())) {
speechHistoryCurrentIndex = speechHistory.length() - 1;
screen_reader_speak(
speech_history_transform_message("Newest message. " + speechHistory[speechHistoryCurrentIndex]), true);
return;
}
int position = speechHistoryCurrentIndex + 1;
screen_reader_speak(speechHistory[speechHistoryCurrentIndex] + " " + position + " of " + speechHistory.length(), true);
}
int position = speechHistoryCurrentIndex + 1;
screen_reader_speak(speech_history_transform_message(speechHistory[speechHistoryCurrentIndex] + " " + position +
" of " + speechHistory.length()),
true);
}
}
string speech_history_latest() {
if (speechHistory.length() == 0) {
return "";
}
return speechHistory[speechHistory.length() - 1];
if (speechHistory.length() == 0) {
return "";
}
return speechHistory[speechHistory.length() - 1];
}

View File

@@ -2,13 +2,13 @@
// Optional compatibility aliases for projects that still call text_reader*.
string text_reader(string content, string title = "Text Reader", bool readOnly = true) {
return file_viewer(content, title, readOnly);
return file_viewer(content, title, readOnly);
}
string text_reader_lines(string[] lines, string title = "Text Reader", bool readOnly = true) {
return file_viewer_lines(lines, title, readOnly);
return file_viewer_lines(lines, title, readOnly);
}
string text_reader_file(string filePath, string title = "", bool readOnly = true) {
return file_viewer_file(filePath, title, readOnly);
return file_viewer_file(filePath, title, readOnly);
}

73
ui.nvgt
View File

@@ -2,53 +2,70 @@
string uiDefaultWindowTitle = "";
bool uiUsePromptAsDialogTitle = true;
funcdef string ui_text_transform_callback(const string& in text);
ui_text_transform_callback @uiTextTransformCallback = null;
void ui_set_text_transform_callback(ui_text_transform_callback @callback) {
@uiTextTransformCallback = @callback;
}
string ui_transform_text(const string& in text) {
if (@uiTextTransformCallback is null)
return text;
return uiTextTransformCallback(text);
}
void ui_set_default_window_title(const string windowTitle) {
uiDefaultWindowTitle = windowTitle;
uiDefaultWindowTitle = windowTitle;
}
void ui_set_use_prompt_as_dialog_title(const bool enabled) {
uiUsePromptAsDialogTitle = enabled;
uiUsePromptAsDialogTitle = enabled;
}
string ui_resolve_dialog_title(const string title, const string prompt) {
if (uiUsePromptAsDialogTitle && prompt != "") {
return prompt;
}
if (uiUsePromptAsDialogTitle && prompt != "") {
return prompt;
}
string dialogTitle = title;
if (dialogTitle == "") {
dialogTitle = prompt;
}
return dialogTitle;
string dialogTitle = title;
if (dialogTitle == "") {
dialogTitle = prompt;
}
return dialogTitle;
}
void ui_restore_window(const string windowTitle = "") {
string resolvedTitle = windowTitle;
if (resolvedTitle == "") {
resolvedTitle = uiDefaultWindowTitle;
}
string resolvedTitle = windowTitle;
if (resolvedTitle == "") {
resolvedTitle = uiDefaultWindowTitle;
}
if (resolvedTitle != "") {
show_window(resolvedTitle);
}
if (resolvedTitle != "") {
show_window(resolvedTitle);
}
}
string ui_input_box(const string title, const string prompt, const string defaultValue = "", const string windowTitle = "") {
string dialogTitle = ui_resolve_dialog_title(title, prompt);
string result = virtual_input_box(dialogTitle, prompt, defaultValue);
ui_restore_window(windowTitle);
return result;
string ui_input_box(const string title, const string prompt, const string defaultValue = "",
const string windowTitle = "") {
string transformedTitle = ui_transform_text(title);
string transformedPrompt = ui_transform_text(prompt);
string dialogTitle = ui_resolve_dialog_title(transformedTitle, transformedPrompt);
string result = virtual_input_box(dialogTitle, transformedPrompt, defaultValue);
ui_restore_window(windowTitle);
return result;
}
int ui_question(const string title, const string prompt, const string windowTitle = "", const bool canCancel = false) {
string dialogTitle = ui_resolve_dialog_title(title, prompt);
int result = virtual_question(dialogTitle, prompt, canCancel);
ui_restore_window(windowTitle);
return result;
string transformedTitle = ui_transform_text(title);
string transformedPrompt = ui_transform_text(prompt);
string dialogTitle = ui_resolve_dialog_title(transformedTitle, transformedPrompt);
int result = virtual_question(dialogTitle, transformedPrompt, canCancel);
ui_restore_window(windowTitle);
return result;
}
void ui_info_box(const string title, const string heading, const string message, const string windowTitle = "") {
virtual_info_box(title, heading, message);
ui_restore_window(windowTitle);
virtual_info_box(ui_transform_text(title), ui_transform_text(heading), ui_transform_text(message));
ui_restore_window(windowTitle);
}

View File

@@ -6,91 +6,98 @@ float volumeControlsMaxDb = 0.0f;
float volumeControlsMinDb = -60.0f;
float volumeControlsStepDb = 3.0f;
float volumeControlsCurrentDb = 0.0f;
volume_controls_apply_callback@ volumeControlsApplyCallback = null;
volume_controls_apply_callback @volumeControlsApplyCallback = null;
void volume_controls_set_apply_callback(volume_controls_apply_callback@ callback) {
@volumeControlsApplyCallback = @callback;
void volume_controls_set_apply_callback(volume_controls_apply_callback @callback) {
@volumeControlsApplyCallback = @callback;
}
void volume_controls_configure(float minDb = -60.0f, float maxDb = 0.0f, float stepDb = 3.0f, float initialDb = 0.0f) {
volumeControlsMinDb = minDb;
volumeControlsMaxDb = maxDb;
if (volumeControlsMaxDb < volumeControlsMinDb) {
float temp = volumeControlsMaxDb;
volumeControlsMaxDb = volumeControlsMinDb;
volumeControlsMinDb = temp;
}
volumeControlsMinDb = minDb;
volumeControlsMaxDb = maxDb;
if (volumeControlsMaxDb < volumeControlsMinDb) {
float temp = volumeControlsMaxDb;
volumeControlsMaxDb = volumeControlsMinDb;
volumeControlsMinDb = temp;
}
if (stepDb <= 0.0f) {
stepDb = 1.0f;
}
volumeControlsStepDb = stepDb;
if (stepDb <= 0.0f) {
stepDb = 1.0f;
}
volumeControlsStepDb = stepDb;
volume_controls_set_current_db(initialDb, false);
volume_controls_set_current_db(initialDb, false);
}
int volume_controls_percent_from_db(float volumeDb) {
float range = volumeControlsMaxDb - volumeControlsMinDb;
if (range <= 0.0f) return 100;
float range = volumeControlsMaxDb - volumeControlsMinDb;
if (range <= 0.0f)
return 100;
float normalized = (volumeDb - volumeControlsMinDb) / range;
int volumePercent = int(normalized * 100.0f + 0.5f);
if (volumePercent < 0) volumePercent = 0;
if (volumePercent > 100) volumePercent = 100;
return volumePercent;
float normalized = (volumeDb - volumeControlsMinDb) / range;
int volumePercent = int(normalized * 100.0f + 0.5f);
if (volumePercent < 0)
volumePercent = 0;
if (volumePercent > 100)
volumePercent = 100;
return volumePercent;
}
void volume_controls_apply(float volumeDb) {
if (@volumeControlsApplyCallback !is null) {
volumeControlsApplyCallback(volumeDb);
return;
}
if (@volumeControlsApplyCallback !is null) {
volumeControlsApplyCallback(volumeDb);
return;
}
// Default engine-global apply behavior when no callback is provided.
sound_master_volume = volumeDb;
// Default engine-global apply behavior when no callback is provided.
sound_master_volume = volumeDb;
}
void volume_controls_set_current_db(float volumeDb, bool announce = true) {
float clamped = volumeDb;
if (clamped > volumeControlsMaxDb) clamped = volumeControlsMaxDb;
if (clamped < volumeControlsMinDb) clamped = volumeControlsMinDb;
float clamped = volumeDb;
if (clamped > volumeControlsMaxDb)
clamped = volumeControlsMaxDb;
if (clamped < volumeControlsMinDb)
clamped = volumeControlsMinDb;
if (clamped == volumeControlsCurrentDb) return;
if (clamped == volumeControlsCurrentDb)
return;
volumeControlsCurrentDb = clamped;
volume_controls_apply(volumeControlsCurrentDb);
volumeControlsCurrentDb = clamped;
volume_controls_apply(volumeControlsCurrentDb);
if (announce) {
int volumePercent = volume_controls_percent_from_db(volumeControlsCurrentDb);
screen_reader_speak("Volume " + volumePercent + ".", true);
}
if (announce) {
int volumePercent = volume_controls_percent_from_db(volumeControlsCurrentDb);
screen_reader_speak("Volume " + volumePercent + ".", true);
}
}
void volume_controls_init_at_max(bool announce = false) {
volume_controls_set_current_db(volumeControlsMaxDb, announce);
volume_controls_set_current_db(volumeControlsMaxDb, announce);
}
float volume_controls_get_current_db() {
return volumeControlsCurrentDb;
return volumeControlsCurrentDb;
}
void volume_controls_handle_keys(int downKey = KEY_PAGEDOWN, int upKey = KEY_PAGEUP, bool announce = true) {
if (key_pressed(downKey)) {
volume_controls_set_current_db(volumeControlsCurrentDb - volumeControlsStepDb, announce);
}
if (key_pressed(upKey)) {
volume_controls_set_current_db(volumeControlsCurrentDb + volumeControlsStepDb, announce);
}
if (key_pressed(downKey)) {
volume_controls_set_current_db(volumeControlsCurrentDb - volumeControlsStepDb, announce);
}
if (key_pressed(upKey)) {
volume_controls_set_current_db(volumeControlsCurrentDb + volumeControlsStepDb, announce);
}
}
void safe_destroy_sound_in_pool(sound_pool@ poolRef, int &inout handle) {
if (handle == -1) return;
if (@poolRef is null) {
handle = -1;
return;
}
if (poolRef.sound_is_active(handle)) {
poolRef.destroy_sound(handle);
}
handle = -1;
void safe_destroy_sound_in_pool(sound_pool @poolRef, int& inout handle) {
if (handle == -1)
return;
if (@poolRef is null) {
handle = -1;
return;
}
if (poolRef.sound_is_active(handle)) {
poolRef.destroy_sound(handle);
}
handle = -1;
}