├── .editorconfig ├── .gitattributes ├── .gitignore ├── API_SCHEMA.md ├── LICENSE-ASSETS.md ├── LICENSE.md ├── README.md ├── addons ├── gd_cubism │ ├── bin │ │ ├── .gitignore │ │ ├── libgd_cubism.windows.debug.x86_64.dll │ │ └── libgd_cubism.windows.release.x86_64.dll │ ├── gd_cubism.gdextension │ └── res │ │ └── shader │ │ ├── 2d_cubism_mask.gdshader │ │ ├── 2d_cubism_mask_add.gdshader │ │ ├── 2d_cubism_mask_add_inv.gdshader │ │ ├── 2d_cubism_mask_mix.gdshader │ │ ├── 2d_cubism_mask_mix_inv.gdshader │ │ ├── 2d_cubism_mask_mul.gdshader │ │ ├── 2d_cubism_mask_mul_inv.gdshader │ │ ├── 2d_cubism_norm_add.gdshader │ │ ├── 2d_cubism_norm_mix.gdshader │ │ └── 2d_cubism_norm_mul.gdshader └── spout-gd │ ├── bin │ ├── .gitignore │ ├── SpoutLibrary.dll │ ├── spout_gd.windows.template_debug.dll │ └── spout_gd.windows.template_release.dll │ └── spout_gd.gdextension ├── assets ├── control_panel │ ├── toast.png │ └── toast.png.import ├── live2d │ ├── Melba_Final2 │ │ ├── Bread Toggle.exp3.json │ │ ├── Confuse Toggle.exp3.json │ │ ├── Confuse.motion3.json │ │ ├── Gymbag Toggle.exp3.json │ │ ├── Idle_1.motion3.json │ │ ├── Idle_2.motion3.json │ │ ├── Idle_3.motion3.json │ │ ├── Sleep.motion3.json │ │ ├── Tears Toggle.exp3.json │ │ ├── Toa Toggle.exp3.json │ │ ├── Void Toggle.exp3.json │ │ ├── melbatoast_model.4096 │ │ │ ├── texture_00.png │ │ │ └── texture_00.png.import │ │ ├── melbatoast_model.cdi3.json │ │ ├── melbatoast_model.moc3 │ │ ├── melbatoast_model.model3.json │ │ └── melbatoast_model.physics3.json │ └── pinnable_assets │ │ ├── censor.png │ │ ├── censor.png.import │ │ ├── glasses.png │ │ ├── glasses.png.import │ │ ├── hatBottom.png │ │ ├── hatBottom.png.import │ │ ├── hatTop.png │ │ ├── hatTop.png.import │ │ ├── pikmin.png │ │ ├── pikmin.png.import │ │ ├── tetoBand.png │ │ └── tetoBand.png.import └── main │ ├── ShantellSans-Bold.ttf │ ├── ShantellSans-Bold.ttf.import │ ├── TOASTED.ogg │ ├── TOASTED.ogg.import │ └── mic │ ├── mic_in.png │ ├── mic_in.png.import │ ├── mic_out.png │ └── mic_out.png.import ├── backend ├── .env.example ├── .gdignore ├── .gitignore ├── README.md ├── backend.py ├── backend_twitch.py ├── chunks │ ├── twister_1.ogg │ ├── twister_2.ogg │ ├── twister_3.ogg │ └── twister_4.ogg └── reqs.txt ├── default_bus_layout.tres ├── dist ├── .gdignore ├── .gitignore ├── config │ ├── .gdignore │ ├── .gitignore │ └── prod.cfg.example ├── run.ps1.example └── songs │ └── .gitignore ├── export_presets.cfg ├── licenses ├── gd-cubism-license ├── godot-license ├── shantell-sans-license ├── spout-gd-license └── spout2-license ├── project.godot ├── readme_assets ├── .gdignore └── interface.png ├── scenes ├── control_panel │ └── control_panel.tscn ├── live2d │ ├── live_2d_melba.tscn │ └── pinnable_assets.tscn └── main │ ├── audio_manager.tscn │ ├── lower_third_manager.tscn │ └── main.tscn ├── scripts ├── control_panel │ ├── control_panel.gd │ ├── helpers.gd │ ├── obs_websocket_client.gd │ └── scroll_container.gd ├── live2d │ ├── classes │ │ ├── animation.gd │ │ ├── pinnable_asset.gd │ │ └── toggle.gd │ ├── eye_blink.gd │ ├── live_2d_melba.gd │ ├── mouth_movement.gd │ ├── pinnable_assets.gd │ └── singing_movement.gd ├── main │ ├── audio_manager.gd │ ├── command_manager.gd │ ├── lower_third.gd │ ├── main.gd │ ├── speech_manager.gd │ └── spout_manager.gd └── shared │ ├── config.gd │ ├── globals.gd │ ├── messagepack.gd │ ├── song.gd │ ├── templates.gd │ ├── variables.gd │ └── websocket_client.gd └── toast.ico /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # General Options 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # GDScript 13 | [*.gd] 14 | indent_size = 4 15 | indent_style = tab 16 | 17 | # Markdown 18 | [*.md] 19 | indent_size = 4 20 | 21 | # Song Subtitles 22 | [subtitles.txt] 23 | indent_size = 4 24 | indent_style = tab 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot tmp files 5 | *.tmp 6 | 7 | # VS Code 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /API_SCHEMA.md: -------------------------------------------------------------------------------- 1 | # API schema 2 | 3 | This document outlines the API schema of communication between backend and Toaster. 4 | 5 | ## Communication 6 | 7 | - Backend - server 8 | - Toaster - client 9 | 10 | We are using Websockets as a transport layer, all the data is wrapped into [MessagePack](https://msgpack.org/) binary messages. 11 | 12 | Default port is 9876. 13 | 14 | ## Schema 15 | 16 | ## Client -> server 17 | 18 | ### Done speaking 19 | 20 | Tells server that the Toaster is done speaking, although doesn't ready for a new message yet. Doesn't require asknowledgment. 21 | 22 | ```json 23 | { 24 | "type": "DoneSpeaking" 25 | } 26 | ``` 27 | 28 | ### Ready for speech 29 | 30 | Tells server that the Toaster is ready to receive a new message. Doesn't require asknowledgment. 31 | 32 | ```json 33 | { 34 | "type": "ReadyForSpeech" 35 | } 36 | ``` 37 | 38 | ## Server -> client 39 | 40 | ### New speech 41 | 42 | Contains the initial prompt and the first response chuck from LLM (including audio in OGG format). Possible `emotions` are listed [here](https://huggingface.co/SamLowe/roberta-base-go_emotions/blob/main/config.json#L14). 43 | 44 | ```json 45 | { 46 | "type": "NewSpeech", 47 | "prompt": "", 48 | "response": "", 49 | "emotions": [""], 50 | "audio": "" 51 | } 52 | ``` 53 | 54 | ### Continue speech 55 | 56 | Contains the subsequence responses from LLM (including audio in OGG format). Must include the initial prompt for tracking purposes. 57 | 58 | ```json 59 | { 60 | "type": "ContinueSpeech", 61 | "prompt": "", 62 | "response": "", 63 | "emotions": [""], 64 | "audio": "" 65 | } 66 | ``` 67 | 68 | ### End speech 69 | 70 | Signals the end of the response from LLM. Must include the initial prompt for tracking purposes. 71 | 72 | ```json 73 | { 74 | "type": "EndSpeech", 75 | "prompt": "" 76 | } 77 | ``` 78 | 79 | ### Play Animation 80 | 81 | Contains the name of animation needs to be applied to the Live2D model 82 | 83 | ```json 84 | { 85 | "type": "PlayAnimation", 86 | "animationName": "", 87 | } 88 | ``` 89 | 90 | ### Set Toggle 91 | 92 | Contains the name of the toggle needs to be applied to the Live2D model, and its desired state. 93 | 94 | ```json 95 | { 96 | "type": "SetTogle", 97 | "toggleName": "", 98 | "enabled": "" 99 | } 100 | ``` 101 | 102 | ### Send Command 103 | 104 | Contains the command from Twitch chat 105 | 106 | ```json 107 | { 108 | "type": "Command", 109 | "command": "" 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toaster 2 | 3 | This is the presentation part of NOM Network's AI VTuber **[Melba Toast](https://www.twitch.tv/melbathetoast/)**. 4 | 5 | Written using [Godot](https://godotengine.org/) and GDScript, this program allows Melba Toast's model to speak, show animations, have expressions and such, driven by the backend. Includes the Control Panel, which can be used to drive the model, OBS Studio and moderate incoming speech. Uses Spout2 to screencap the model, includes native Color Key mode for screen sharing. 6 | 7 | Communication with the backend follows [this API schema](API_SCHEMA.md). 8 | 9 | **Control panel interface** 10 | ![Interface](readme_assets/interface.png?1) 11 | 12 | ## Going live 13 | 14 | This [Wiki page](https://github.com/NOM-Network/Melba-Toaster/wiki/Going-live) contains instructions on how to go live using the [Release version of the Toaster](https://github.com/NOM-Network/Melba-Toaster/releases). 15 | 16 | ### Song support 17 | 18 | Melba can sing! Song support is outlined in [Wiki](https://github.com/NOM-Network/Melba-Toaster/wiki/Song-support). 19 | 20 | ## Development 21 | 22 | 1. Install the latest minor version of Godot 4.3: 23 | 24 | - standalone: 25 | - via winget: 26 | 27 | ```powershell 28 | winget install GodotEngine.GodotEngine 29 | ``` 30 | 31 | 2. Clone this repo via Git: 32 | 33 | ```bash 34 | git clone https://github.com/NOM-Network/Melba-Toaster.git 35 | ``` 36 | 37 | Alternatively, you can Download ZIP package using the green "Code" button and unzip it. 38 | 39 | 3. In the `dist/config` folder, duplicate `prod.cfg.example` file, rename it to `debug.cfg` and fill it out with the connection details for both OBS and backend websockets (make sure they are available). 40 | 41 | 4. Open the project in Godot. 42 | 43 | 5. Hit F5 in Godot editor. Live2D and Control Panel scenes should start automatically. 44 | 45 | > The project can run without OBS and/or the backend, but nothing will actually happen. This can be useful for testing the songs. If you need to test yapping capabilities of the model, use the [mock backend server](backend/README.md) in the `backend` folder. 46 | > 47 | > When pushing changes to the repository, ignore or revert any `Param` changes in `scenes\live2d\live_2d_melba.tscn` - they are control parameters for the model and are changed in the runtime. If you use GitHub Desktop, you can ignore these lines from commit by clicking on the line block. 48 | 49 | ### Note for Mac/Linux users 50 | 51 | This project is built for use on Windows and uses Windows libraries for Cubism extension. If you need to use Toaster on Mac or Linux, you have to [build the extension first](https://github.com/MizunagiKB/gd_cubism/blob/main/docs/BUILD.en.adoc), then put the files in `addons/gd_cubism/bin` folder. 52 | 53 | ### Build machine perparations 54 | 55 | Moved to [Wiki](https://github.com/NOM-Network/Melba-Toaster/wiki/Build-machine-perparations) 56 | 57 | ## License 58 | 59 | Melba Toast © 2023-present NOM Network and contributors. 60 | 61 | Project codebase is licensed under a [AGPL 3.0 (and later) license](LICENSE.md). 62 | 63 | Art assets are licensed under a [CC BY-SA 4.0 license](LICENSE-ASSETS.md). 64 | 65 | ## Acknowledgements 66 | 67 | This project uses the following 3rd party libraries and assets: 68 | 69 | - [Godot Engine](https://godotengine.org), licensed under the MIT license. 70 | 71 | - [Cubism for GDScript](https://github.com/MizunagiKB/gd_cubism) by MizunagiKB, licensed under the MIT license. 72 | 73 | - [Live2D Cubism SDK](https://github.com/Live2D/CubismNativeFramework) by Live2D, licensed under the [Cubism SDK Release License](https://www.live2d.com/en/sdk/license). 74 | 75 | - [Shantell Sans font](https://shantellsans.com), licensed under the SIL Open Font License, Version 1.1. 76 | 77 | - [Spout GD](https://github.com/you-win/spout-gd), licensed under the MPL 2.0 license. 78 | 79 | - [Spout2](https://github.com/leadedge/Spout2), licensed under the BSD 2-Clause license. 80 | 81 | License files are available in [licenses/](licenses/) folder of this repository. 82 | -------------------------------------------------------------------------------- /addons/gd_cubism/bin/.gitignore: -------------------------------------------------------------------------------- 1 | ~* 2 | *.exp 3 | *.lib 4 | -------------------------------------------------------------------------------- /addons/gd_cubism/bin/libgd_cubism.windows.debug.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/addons/gd_cubism/bin/libgd_cubism.windows.debug.x86_64.dll -------------------------------------------------------------------------------- /addons/gd_cubism/bin/libgd_cubism.windows.release.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/addons/gd_cubism/bin/libgd_cubism.windows.release.x86_64.dll -------------------------------------------------------------------------------- /addons/gd_cubism/gd_cubism.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | 3 | entry_symbol = "gd_cubism_library_init" 4 | compatibility_minimum = 4.3 5 | 6 | [libraries] 7 | 8 | macos.debug = "bin/libgd_cubism.macos.debug.framework" 9 | macos.release = "bin/libgd_cubism.macos.release.framework" 10 | windows.debug.x86_64 = "bin/libgd_cubism.windows.debug.x86_64.dll" 11 | windows.release.x86_64 = "bin/libgd_cubism.windows.release.x86_64.dll" 12 | linux.debug.x86_64 = "bin/libgd_cubism.linux.debug.x86_64.so" 13 | linux.release.x86_64 = "bin/libgd_cubism.linux.release.x86_64.so" 14 | # linux.debug.arm64 = "bin/libgd_cubism.linux.debug.arm64.so" 15 | # linux.release.arm64 = "bin/libgd_cubism.linux.release.arm64.so" 16 | # linux.debug.rv64 = "bin/libgd_cubism.linux.debug.rv64.so" 17 | # linux.release.rv64 = "bin/libgd_cubism.linux.release.rv64.so" 18 | ios.debug.arm64 = "bin/libgd_cubism.ios.debug.arm64.dylib" 19 | ios.release.arm64 = "bin/libgd_cubism.ios.release.arm64.dylib" 20 | ios.debug.universal = "bin/libgd_cubism.ios.debug.universal.dylib" 21 | ios.release.universal = "bin/libgd_cubism.ios.release.universal.dylib" 22 | android.debug.arm32 = "bin/libgd_cubism.android.debug.arm32.so" 23 | android.release.arm32 = "bin/libgd_cubism.android.release.arm32.so" 24 | android.debug.arm64 = "bin/libgd_cubism.android.debug.arm64.so" 25 | android.release.arm64 = "bin/libgd_cubism.android.release.arm64.so" 26 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask 4 | shader_type canvas_item; 5 | render_mode blend_mix, unshaded; 6 | 7 | uniform vec4 channel; 8 | uniform sampler2D tex_main : filter_linear_mipmap; 9 | 10 | void vertex() { 11 | UV.y = 1.0 - UV.y; 12 | } 13 | 14 | void fragment() { 15 | COLOR = channel * texture(tex_main, UV).a; 16 | } 17 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_add.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + Add 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask.rgb = color_for_mask.rgb * mask_val; 60 | COLOR = vec4(color_for_mask.rgb, 0.0); 61 | } 62 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_add_inv.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + AddInv 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask.rgb = color_for_mask.rgb * (1.0 - mask_val); 60 | COLOR = vec4(color_for_mask.rgb, 0.0); 61 | } 62 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_mix.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + Mix 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask = color_for_mask * mask_val; 60 | COLOR = color_for_mask; 61 | } 62 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_mix_inv.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + MixInv 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask = color_for_mask * (1.0 - mask_val); 60 | COLOR = color_for_mask; 61 | } 62 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_mul.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + Mul 4 | shader_type canvas_item; 5 | render_mode blend_mul, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask = color_for_mask * mask_val; 60 | COLOR = vec4( 61 | color_for_mask.r + (1.0 - color_for_mask.a), 62 | color_for_mask.g + (1.0 - color_for_mask.a), 63 | color_for_mask.b + (1.0 - color_for_mask.a), 64 | 1.0 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_mask_mul_inv.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mask + MulInv 4 | shader_type canvas_item; 5 | render_mode blend_mul, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | uniform sampler2D tex_mask : filter_linear_mipmap; 13 | 14 | uniform bool auto_scale; 15 | uniform vec2 canvas_size; 16 | uniform vec2 mask_size; 17 | uniform float ratio; 18 | uniform vec2 adjust_pos; 19 | uniform float adjust_scale; 20 | 21 | 22 | void vertex() { 23 | UV.y = 1.0 - UV.y; 24 | } 25 | 26 | vec2 lookup_mask_uv(vec2 screen_uv) { 27 | 28 | if(auto_scale == false) return screen_uv; 29 | 30 | vec2 r_uv = screen_uv - 0.5; 31 | vec2 calc_pos; 32 | 33 | calc_pos.x = (canvas_size.x * adjust_scale) - canvas_size.x; 34 | calc_pos.x = calc_pos.x * (adjust_pos.x / canvas_size.x); 35 | calc_pos.x = calc_pos.x / canvas_size.x; 36 | 37 | calc_pos.y = (canvas_size.y * adjust_scale) - canvas_size.y; 38 | calc_pos.y = calc_pos.y * (adjust_pos.y / canvas_size.y); 39 | calc_pos.y = calc_pos.y / canvas_size.y; 40 | 41 | r_uv = r_uv + calc_pos; 42 | r_uv = r_uv * (1.0 / adjust_scale); 43 | 44 | return r_uv + 0.5; 45 | } 46 | 47 | void fragment() { 48 | vec4 color_tex = texture(tex_main, UV); 49 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 50 | 51 | // premul alpha 52 | color_tex.rgb = color_tex.rgb + color_screen.rgb - (color_tex.rgb * color_screen.rgb); 53 | vec4 color_for_mask = color_tex * color_base; 54 | color_for_mask.rgb = color_for_mask.rgb * color_for_mask.a; 55 | 56 | vec4 clip_mask = texture(tex_mask, lookup_mask_uv(SCREEN_UV)) * channel; 57 | 58 | float mask_val = clip_mask.r + clip_mask.g + clip_mask.b + clip_mask.a; 59 | color_for_mask = color_for_mask * (1.0 - mask_val); 60 | COLOR = vec4( 61 | color_for_mask.r + (1.0 - color_for_mask.a), 62 | color_for_mask.g + (1.0 - color_for_mask.a), 63 | color_for_mask.b + (1.0 - color_for_mask.a), 64 | 1.0 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_norm_add.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Add 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | 13 | void vertex() { 14 | UV.y = 1.0 - UV.y; 15 | } 16 | 17 | void fragment() { 18 | vec4 color_tex = texture(tex_main, UV); 19 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 20 | 21 | // premul alpha 22 | color_tex.rgb = (color_tex.rgb + color_screen.rgb) - (color_tex.rgb * color_screen.rgb); 23 | vec4 color = color_tex * color_base; 24 | COLOR = vec4(color.rgb * color.a, 0.0); 25 | } 26 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_norm_mix.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mix 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | 13 | void vertex() { 14 | UV.y = 1.0 - UV.y; 15 | } 16 | 17 | void fragment() { 18 | vec4 color_tex = texture(tex_main, UV); 19 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 20 | 21 | // premul alpha 22 | color_tex.rgb = (color_tex.rgb + color_screen.rgb) - (color_tex.rgb * color_screen.rgb); 23 | vec4 color = color_tex * color_base; 24 | COLOR = vec4(color.rgb * color.a, color.a); 25 | } 26 | -------------------------------------------------------------------------------- /addons/gd_cubism/res/shader/2d_cubism_norm_mul.gdshader: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: 2023 MizunagiKB 3 | // GDCubism shader: Mul 4 | shader_type canvas_item; 5 | render_mode blend_premul_alpha, unshaded; 6 | 7 | uniform vec4 color_base; 8 | uniform vec4 color_screen; 9 | uniform vec4 color_multiply; 10 | uniform vec4 channel; 11 | uniform sampler2D tex_main : filter_linear_mipmap; 12 | 13 | void vertex() { 14 | UV.y = 1.0 - UV.y; 15 | } 16 | 17 | void fragment() { 18 | vec4 color_tex = texture(tex_main, UV); 19 | color_tex.rgb = color_tex.rgb * color_multiply.rgb; 20 | 21 | // premul alpha 22 | color_tex.rgb = (color_tex.rgb + color_screen.rgb) - (color_tex.rgb * color_screen.rgb); 23 | vec4 color = color_tex * color_base; 24 | COLOR = vec4(color.rgb * color.a, color.a); 25 | } 26 | -------------------------------------------------------------------------------- /addons/spout-gd/bin/.gitignore: -------------------------------------------------------------------------------- 1 | ~* 2 | *.exp 3 | *.lib 4 | -------------------------------------------------------------------------------- /addons/spout-gd/bin/SpoutLibrary.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/addons/spout-gd/bin/SpoutLibrary.dll -------------------------------------------------------------------------------- /addons/spout-gd/bin/spout_gd.windows.template_debug.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/addons/spout-gd/bin/spout_gd.windows.template_debug.dll -------------------------------------------------------------------------------- /addons/spout-gd/bin/spout_gd.windows.template_release.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/addons/spout-gd/bin/spout_gd.windows.template_release.dll -------------------------------------------------------------------------------- /addons/spout-gd/spout_gd.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | entry_symbol = "spout_gd_library_init" 3 | compatibility_minimum = 4.3 4 | 5 | [libraries] 6 | windows.debug.x86_64 = "bin/spout_gd.windows.template_debug.dll" 7 | windows.release.x86_64 = "bin/spout_gd.windows.template_release.dll" 8 | 9 | [dependencies] 10 | windows.debug = { 11 | "bin/SpoutLibrary.dll": "" 12 | } 13 | windows.release = { 14 | "bin/SpoutLibrary.dll": "" 15 | } 16 | -------------------------------------------------------------------------------- /assets/control_panel/toast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/control_panel/toast.png -------------------------------------------------------------------------------- /assets/control_panel/toast.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cobm5phwndmwn" 6 | path.s3tc="res://.godot/imported/toast.png-2d071d401c658780c643f9688cc50b3e.s3tc.ctex" 7 | path.etc2="res://.godot/imported/toast.png-2d071d401c658780c643f9688cc50b3e.etc2.ctex" 8 | metadata={ 9 | "imported_formats": ["s3tc_bptc", "etc2_astc"], 10 | "vram_texture": true 11 | } 12 | 13 | [deps] 14 | 15 | source_file="res://assets/control_panel/toast.png" 16 | dest_files=["res://.godot/imported/toast.png-2d071d401c658780c643f9688cc50b3e.s3tc.ctex", "res://.godot/imported/toast.png-2d071d401c658780c643f9688cc50b3e.etc2.ctex"] 17 | 18 | [params] 19 | 20 | compress/mode=2 21 | compress/high_quality=false 22 | compress/lossy_quality=0.7 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=true 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/fix_alpha_border=true 31 | process/premult_alpha=false 32 | process/normal_map_invert_y=false 33 | process/hdr_as_srgb=false 34 | process/hdr_clamp_exposure=false 35 | process/size_limit=0 36 | detect_3d/compress_to=0 37 | -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Bread Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param9", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Confuse Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param18", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Confuse.motion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "Duration": 10, 5 | "Fps": 30.0, 6 | "Loop": true, 7 | "AreBeziersRestricted": true, 8 | "CurveCount": 1, 9 | "TotalSegmentCount": 23, 10 | "TotalPointCount": 24, 11 | "UserDataCount": 0, 12 | "TotalUserDataSize": 0 13 | }, 14 | "Curves": [ 15 | { 16 | "Target": "Parameter", 17 | "Id": "Param19", 18 | "Segments": [ 19 | 0, 20 | -180, 21 | 0, 22 | 0.833, 23 | 180, 24 | 0, 25 | 0.867, 26 | -180, 27 | 0, 28 | 1.7, 29 | 180, 30 | 0, 31 | 1.733, 32 | -180, 33 | 0, 34 | 2.567, 35 | 180, 36 | 0, 37 | 2.6, 38 | -180, 39 | 0, 40 | 3.433, 41 | 180, 42 | 0, 43 | 3.467, 44 | -180, 45 | 0, 46 | 4.3, 47 | 180, 48 | 0, 49 | 4.333, 50 | -180, 51 | 0, 52 | 5.167, 53 | 180, 54 | 0, 55 | 5.2, 56 | -180, 57 | 0, 58 | 6.033, 59 | 180, 60 | 0, 61 | 6.067, 62 | -180, 63 | 0, 64 | 6.9, 65 | 180, 66 | 0, 67 | 6.933, 68 | -180, 69 | 0, 70 | 7.767, 71 | 180, 72 | 0, 73 | 7.8, 74 | -180, 75 | 0, 76 | 8.633, 77 | 180, 78 | 0, 79 | 8.667, 80 | -180, 81 | 0, 82 | 9.5, 83 | 180, 84 | 0, 85 | 9.533, 86 | -180, 87 | 0, 88 | 10, 89 | 21.6 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Gymbag Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param28", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Tears Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param20", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Toa Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param21", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/Void Toggle.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "Param14", 6 | "Value": 1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/melbatoast_model.4096/texture_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/Melba_Final2/melbatoast_model.4096/texture_00.png -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/melbatoast_model.4096/texture_00.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b7koqu2lnfhs6" 6 | path="res://.godot/imported/texture_00.png-3c87971baaef896d9603313c914d0e8d.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/Melba_Final2/melbatoast_model.4096/texture_00.png" 14 | dest_files=["res://.godot/imported/texture_00.png-3c87971baaef896d9603313c914d0e8d.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/melbatoast_model.cdi3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Parameters": [ 4 | { 5 | "Id": "ParamAngleX", 6 | "GroupId": "ParamGroup", 7 | "Name": "Angle X" 8 | }, 9 | { 10 | "Id": "ParamAngleY", 11 | "GroupId": "ParamGroup", 12 | "Name": "Angle Y" 13 | }, 14 | { 15 | "Id": "ParamAngleZ", 16 | "GroupId": "ParamGroup", 17 | "Name": "Angle Z" 18 | }, 19 | { 20 | "Id": "ParamBodyAngleX", 21 | "GroupId": "ParamGroup", 22 | "Name": "Body X" 23 | }, 24 | { 25 | "Id": "ParamBodyAngleY", 26 | "GroupId": "ParamGroup", 27 | "Name": "Body Y" 28 | }, 29 | { 30 | "Id": "ParamBodyAngleZ", 31 | "GroupId": "ParamGroup", 32 | "Name": "Body Z" 33 | }, 34 | { 35 | "Id": "Param3", 36 | "GroupId": "ParamGroup3", 37 | "Name": "angle x physics" 38 | }, 39 | { 40 | "Id": "Param4", 41 | "GroupId": "ParamGroup3", 42 | "Name": "angle y physics" 43 | }, 44 | { 45 | "Id": "Param5", 46 | "GroupId": "ParamGroup3", 47 | "Name": "angle z physics" 48 | }, 49 | { 50 | "Id": "ParamEyeLOpen", 51 | "GroupId": "ParamGroup2", 52 | "Name": "EyeL Open" 53 | }, 54 | { 55 | "Id": "ParamEyeLSmile", 56 | "GroupId": "ParamGroup2", 57 | "Name": "EyeL Smile" 58 | }, 59 | { 60 | "Id": "ParamEyeROpen", 61 | "GroupId": "ParamGroup2", 62 | "Name": "EyeR Open" 63 | }, 64 | { 65 | "Id": "ParamEyeRSmile", 66 | "GroupId": "ParamGroup2", 67 | "Name": "EyeR Smile" 68 | }, 69 | { 70 | "Id": "ParamEyeBallX", 71 | "GroupId": "ParamGroup2", 72 | "Name": "Eyeball X" 73 | }, 74 | { 75 | "Id": "ParamEyeBallY", 76 | "GroupId": "ParamGroup2", 77 | "Name": "Eyeball Y" 78 | }, 79 | { 80 | "Id": "ParamMouthForm", 81 | "GroupId": "ParamGroup4", 82 | "Name": "Mouth Form" 83 | }, 84 | { 85 | "Id": "ParamMouthOpenY", 86 | "GroupId": "ParamGroup4", 87 | "Name": "Mouth Open" 88 | }, 89 | { 90 | "Id": "Param6", 91 | "GroupId": "ParamGroup5", 92 | "Name": "x 1" 93 | }, 94 | { 95 | "Id": "Param7", 96 | "GroupId": "ParamGroup5", 97 | "Name": "x 2" 98 | }, 99 | { 100 | "Id": "Param8", 101 | "GroupId": "ParamGroup5", 102 | "Name": "x 3" 103 | }, 104 | { 105 | "Id": "Param10", 106 | "GroupId": "ParamGroup5", 107 | "Name": "y 1" 108 | }, 109 | { 110 | "Id": "Param11", 111 | "GroupId": "ParamGroup5", 112 | "Name": "y 2" 113 | }, 114 | { 115 | "Id": "Param15", 116 | "GroupId": "ParamGroup7", 117 | "Name": "Body x physics" 118 | }, 119 | { 120 | "Id": "Param16", 121 | "GroupId": "ParamGroup7", 122 | "Name": "Body y physics" 123 | }, 124 | { 125 | "Id": "Param17", 126 | "GroupId": "ParamGroup7", 127 | "Name": "Body z physics" 128 | }, 129 | { 130 | "Id": "Param9", 131 | "GroupId": "ParamGroup6", 132 | "Name": "Bread" 133 | }, 134 | { 135 | "Id": "Param14", 136 | "GroupId": "ParamGroup6", 137 | "Name": "Void" 138 | }, 139 | { 140 | "Id": "Param18", 141 | "GroupId": "ParamGroup6", 142 | "Name": "Confuse" 143 | }, 144 | { 145 | "Id": "Param21", 146 | "GroupId": "ParamGroup6", 147 | "Name": "Toa Toggle" 148 | }, 149 | { 150 | "Id": "Param28", 151 | "GroupId": "ParamGroup6", 152 | "Name": "Gymbag" 153 | }, 154 | { 155 | "Id": "Param19", 156 | "GroupId": "", 157 | "Name": "Confuse loop" 158 | }, 159 | { 160 | "Id": "Param20", 161 | "GroupId": "", 162 | "Name": "Tear" 163 | }, 164 | { 165 | "Id": "Param24", 166 | "GroupId": "", 167 | "Name": "Gymbag physic x" 168 | }, 169 | { 170 | "Id": "Param26", 171 | "GroupId": "", 172 | "Name": "Gymbag physic x2" 173 | }, 174 | { 175 | "Id": "Param25", 176 | "GroupId": "", 177 | "Name": "Gymbag physic y" 178 | }, 179 | { 180 | "Id": "Param27", 181 | "GroupId": "", 182 | "Name": "Gymbag physic z" 183 | }, 184 | { 185 | "Id": "ParamBrowLY", 186 | "GroupId": "", 187 | "Name": "BrowL Y" 188 | }, 189 | { 190 | "Id": "ParamBrowRY", 191 | "GroupId": "", 192 | "Name": "BrowR Y" 193 | }, 194 | { 195 | "Id": "ParamBrowLX", 196 | "GroupId": "", 197 | "Name": "BrowL X" 198 | }, 199 | { 200 | "Id": "ParamBrowRX", 201 | "GroupId": "", 202 | "Name": "BrowR X" 203 | }, 204 | { 205 | "Id": "ParamBrowLAngle", 206 | "GroupId": "", 207 | "Name": "BrowL Angle" 208 | }, 209 | { 210 | "Id": "ParamBrowRAngle", 211 | "GroupId": "", 212 | "Name": "BrowR Angle" 213 | }, 214 | { 215 | "Id": "ParamBrowLForm", 216 | "GroupId": "", 217 | "Name": "BrowL Form" 218 | }, 219 | { 220 | "Id": "ParamBrowRForm", 221 | "GroupId": "", 222 | "Name": "BrowR Form" 223 | }, 224 | { 225 | "Id": "ParamCheek", 226 | "GroupId": "", 227 | "Name": "Cheek" 228 | }, 229 | { 230 | "Id": "ParamBreath", 231 | "GroupId": "", 232 | "Name": "Breath" 233 | }, 234 | { 235 | "Id": "ParamHairFront", 236 | "GroupId": "", 237 | "Name": "Hair Move Front" 238 | }, 239 | { 240 | "Id": "ParamHairSide", 241 | "GroupId": "", 242 | "Name": "Hair Move Side" 243 | }, 244 | { 245 | "Id": "ParamHairBack", 246 | "GroupId": "", 247 | "Name": "Hair Move Back" 248 | }, 249 | { 250 | "Id": "Param", 251 | "GroupId": "", 252 | "Name": "Jiggle x" 253 | }, 254 | { 255 | "Id": "Param2", 256 | "GroupId": "", 257 | "Name": "Jiggle y" 258 | }, 259 | { 260 | "Id": "Param12", 261 | "GroupId": "", 262 | "Name": "Jiggle Bread x" 263 | }, 264 | { 265 | "Id": "Param13", 266 | "GroupId": "", 267 | "Name": "Jiggle Bread y" 268 | }, 269 | { 270 | "Id": "Param23", 271 | "GroupId": "", 272 | "Name": "Jiggle Toa x" 273 | }, 274 | { 275 | "Id": "Param22", 276 | "GroupId": "", 277 | "Name": "Jiggle Toa y" 278 | } 279 | ], 280 | "ParameterGroups": [ 281 | { 282 | "Id": "ParamGroup", 283 | "GroupId": "", 284 | "Name": "Inputs" 285 | }, 286 | { 287 | "Id": "ParamGroup3", 288 | "GroupId": "", 289 | "Name": "Angle" 290 | }, 291 | { 292 | "Id": "ParamGroup2", 293 | "GroupId": "", 294 | "Name": "Eyes" 295 | }, 296 | { 297 | "Id": "ParamGroup4", 298 | "GroupId": "", 299 | "Name": "Mouth" 300 | }, 301 | { 302 | "Id": "ParamGroup5", 303 | "GroupId": "", 304 | "Name": "Hair" 305 | }, 306 | { 307 | "Id": "ParamGroup7", 308 | "GroupId": "", 309 | "Name": "Body" 310 | }, 311 | { 312 | "Id": "ParamGroup6", 313 | "GroupId": "", 314 | "Name": "Toggles" 315 | } 316 | ], 317 | "Parts": [ 318 | { 319 | "Id": "Part35", 320 | "Name": "melbatoast_model.psd (Corresponding layer not found)" 321 | }, 322 | { 323 | "Id": "Part33", 324 | "Name": "melbatoast_model.psd (Corresponding layer not found)" 325 | }, 326 | { 327 | "Id": "Part31", 328 | "Name": "melbatoast_model.psd (Corresponding layer not found)" 329 | }, 330 | { 331 | "Id": "Part29", 332 | "Name": "melbatoast_model.psd (Corresponding layer not found)" 333 | }, 334 | { 335 | "Id": "Model", 336 | "Name": "Model" 337 | }, 338 | { 339 | "Id": "Sketch", 340 | "Name": "Sketch" 341 | }, 342 | { 343 | "Id": "Model5", 344 | "Name": "Model" 345 | }, 346 | { 347 | "Id": "Model4", 348 | "Name": "Model" 349 | }, 350 | { 351 | "Id": "Model3", 352 | "Name": "Model" 353 | }, 354 | { 355 | "Id": "Model2", 356 | "Name": "Model" 357 | }, 358 | { 359 | "Id": "Head", 360 | "Name": "Head" 361 | }, 362 | { 363 | "Id": "Part4", 364 | "Name": "Drill hairs" 365 | }, 366 | { 367 | "Id": "Part5", 368 | "Name": "Upper body" 369 | }, 370 | { 371 | "Id": "Neck", 372 | "Name": "Neck" 373 | }, 374 | { 375 | "Id": "Part10", 376 | "Name": "back hair" 377 | }, 378 | { 379 | "Id": "Part11", 380 | "Name": "Lower body" 381 | }, 382 | { 383 | "Id": "Toast", 384 | "Name": "Toast" 385 | }, 386 | { 387 | "Id": "Part14", 388 | "Name": "Folder 2" 389 | }, 390 | { 391 | "Id": "color2", 392 | "Name": "color2" 393 | }, 394 | { 395 | "Id": "Part15", 396 | "Name": "Folder 1" 397 | }, 398 | { 399 | "Id": "Gymbag", 400 | "Name": "Gymbag" 401 | }, 402 | { 403 | "Id": "Confuse2", 404 | "Name": "Confuse" 405 | }, 406 | { 407 | "Id": "Tears2", 408 | "Name": "Tears" 409 | }, 410 | { 411 | "Id": "Head5", 412 | "Name": "Head" 413 | }, 414 | { 415 | "Id": "Part22", 416 | "Name": "Drill hairs" 417 | }, 418 | { 419 | "Id": "Part23", 420 | "Name": "Upper body" 421 | }, 422 | { 423 | "Id": "Neck2", 424 | "Name": "Neck" 425 | }, 426 | { 427 | "Id": "Part28", 428 | "Name": "back hair" 429 | }, 430 | { 431 | "Id": "Part30", 432 | "Name": "Lower body" 433 | }, 434 | { 435 | "Id": "Head4", 436 | "Name": "Head" 437 | }, 438 | { 439 | "Id": "Head3", 440 | "Name": "Head" 441 | }, 442 | { 443 | "Id": "Head2", 444 | "Name": "Head" 445 | }, 446 | { 447 | "Id": "Hair", 448 | "Name": "Hair" 449 | }, 450 | { 451 | "Id": "Face", 452 | "Name": "Face" 453 | }, 454 | { 455 | "Id": "Part3", 456 | "Name": "Main hair" 457 | }, 458 | { 459 | "Id": "Bread", 460 | "Name": "Bread" 461 | }, 462 | { 463 | "Id": "Part17", 464 | "Name": "Drill hairs" 465 | }, 466 | { 467 | "Id": "Body", 468 | "Name": "Body" 469 | }, 470 | { 471 | "Id": "Part12", 472 | "Name": "R leg" 473 | }, 474 | { 475 | "Id": "Part13", 476 | "Name": "L leg" 477 | }, 478 | { 479 | "Id": "Hair2", 480 | "Name": "Hair" 481 | }, 482 | { 483 | "Id": "Face2", 484 | "Name": "Face" 485 | }, 486 | { 487 | "Id": "Part21", 488 | "Name": "Main hair" 489 | }, 490 | { 491 | "Id": "Bread2", 492 | "Name": "Bread" 493 | }, 494 | { 495 | "Id": "Body3", 496 | "Name": "Body" 497 | }, 498 | { 499 | "Id": "Part19", 500 | "Name": "Toa-chan_1" 501 | }, 502 | { 503 | "Id": "Part18", 504 | "Name": "Toa-chan_1" 505 | }, 506 | { 507 | "Id": "Part16", 508 | "Name": "Toa-chan_1" 509 | }, 510 | { 511 | "Id": "Eyes", 512 | "Name": "Eyes" 513 | }, 514 | { 515 | "Id": "Mount", 516 | "Name": "Mount" 517 | }, 518 | { 519 | "Id": "Body2", 520 | "Name": "Body" 521 | }, 522 | { 523 | "Id": "Arms", 524 | "Name": "Arms" 525 | }, 526 | { 527 | "Id": "Eyes2", 528 | "Name": "Eyes" 529 | }, 530 | { 531 | "Id": "Body4", 532 | "Name": "Body" 533 | }, 534 | { 535 | "Id": "Arms2", 536 | "Name": "Arms" 537 | }, 538 | { 539 | "Id": "Toa", 540 | "Name": "Toa" 541 | }, 542 | { 543 | "Id": "Tears", 544 | "Name": "Tears" 545 | }, 546 | { 547 | "Id": "Confuse", 548 | "Name": "Confuse" 549 | }, 550 | { 551 | "Id": "Part6", 552 | "Name": "Cloth 2" 553 | }, 554 | { 555 | "Id": "Part7", 556 | "Name": "Skirt 2" 557 | }, 558 | { 559 | "Id": "Part8", 560 | "Name": "R arm" 561 | }, 562 | { 563 | "Id": "Part9", 564 | "Name": "L arm" 565 | }, 566 | { 567 | "Id": "Part24", 568 | "Name": "Cloth 2" 569 | }, 570 | { 571 | "Id": "Part26", 572 | "Name": "R arm" 573 | }, 574 | { 575 | "Id": "Part27", 576 | "Name": "L arm" 577 | } 578 | ], 579 | "CombinedParameters": [ 580 | [ 581 | "ParamMouthForm", 582 | "ParamMouthOpenY" 583 | ] 584 | ] 585 | } -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/melbatoast_model.moc3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/Melba_Final2/melbatoast_model.moc3 -------------------------------------------------------------------------------- /assets/live2d/Melba_Final2/melbatoast_model.model3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "FileReferences": { 4 | "Moc": "melbatoast_model.moc3", 5 | "Textures": [ 6 | "melbatoast_model.4096/texture_00.png" 7 | ], 8 | "Physics": "melbatoast_model.physics3.json", 9 | "DisplayInfo": "melbatoast_model.cdi3.json", 10 | "Expressions": [ 11 | { 12 | "Name": "Bread", 13 | "File": "Bread Toggle.exp3.json" 14 | }, 15 | { 16 | "Name": "Void", 17 | "File": "Void Toggle.exp3.json" 18 | }, 19 | { 20 | "Name": "Confuse", 21 | "File": "Confuse Toggle.exp3.json" 22 | }, 23 | { 24 | "Name": "Tears", 25 | "File": "Tears Toggle.exp3.json" 26 | }, 27 | { 28 | "Name": "Toa", 29 | "File": "Toa Toggle.exp3.json" 30 | }, 31 | { 32 | "Name": "Gymbag", 33 | "File": "Gymbag Toggle.exp3.json" 34 | } 35 | ], 36 | "Motions": { 37 | "": [ 38 | { 39 | "File": "Idle_1.motion3.json" 40 | }, 41 | { 42 | "File": "Idle_2.motion3.json" 43 | }, 44 | { 45 | "File": "Idle_3.motion3.json" 46 | }, 47 | { 48 | "File": "Sleep.motion3.json" 49 | }, 50 | { 51 | "File": "Confuse.motion3.json" 52 | } 53 | ] 54 | } 55 | }, 56 | "Groups": [ 57 | { 58 | "Target": "Parameter", 59 | "Name": "EyeBlink", 60 | "Ids": [] 61 | }, 62 | { 63 | "Target": "Parameter", 64 | "Name": "LipSync", 65 | "Ids": [] 66 | } 67 | ], 68 | "HitAreas": [] 69 | } 70 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/censor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/censor.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/censor.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dow7ry1r7bvdm" 6 | path="res://.godot/imported/censor.png-f74c734ef0cf6eacf4da98c07c98467e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/censor.png" 14 | dest_files=["res://.godot/imported/censor.png-f74c734ef0cf6eacf4da98c07c98467e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=3 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/glasses.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/glasses.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cym7l00g0fswe" 6 | path="res://.godot/imported/glasses.png-8f9a3544b54eb5c4001c56c10f29a08a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/glasses.png" 14 | dest_files=["res://.godot/imported/glasses.png-8f9a3544b54eb5c4001c56c10f29a08a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=3 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/hatBottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/hatBottom.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/hatBottom.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cyg5rr7kbpiaq" 6 | path="res://.godot/imported/hatBottom.png-3702b4e6ad811c8fbe9eb55694a12abc.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/hatBottom.png" 14 | dest_files=["res://.godot/imported/hatBottom.png-3702b4e6ad811c8fbe9eb55694a12abc.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=3 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/hatTop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/hatTop.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/hatTop.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://q7sygse7kwg5" 6 | path="res://.godot/imported/hatTop.png-e554b03777d2bfd5aa6a1011680ed4b1.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/hatTop.png" 14 | dest_files=["res://.godot/imported/hatTop.png-e554b03777d2bfd5aa6a1011680ed4b1.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=3 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/pikmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/pikmin.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/pikmin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dx4faee3ujvah" 6 | path="res://.godot/imported/pikmin.png-0438482f7a51120ac4a65ba292cb0fe4.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/pikmin.png" 14 | dest_files=["res://.godot/imported/pikmin.png-0438482f7a51120ac4a65ba292cb0fe4.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=true 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/tetoBand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/live2d/pinnable_assets/tetoBand.png -------------------------------------------------------------------------------- /assets/live2d/pinnable_assets/tetoBand.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dqge66kutwpnq" 6 | path="res://.godot/imported/tetoBand.png-69aac670bbfcca42594391a9f96efd37.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/live2d/pinnable_assets/tetoBand.png" 14 | dest_files=["res://.godot/imported/tetoBand.png-69aac670bbfcca42594391a9f96efd37.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/main/ShantellSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/main/ShantellSans-Bold.ttf -------------------------------------------------------------------------------- /assets/main/ShantellSans-Bold.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://dljb036lelm30" 6 | path="res://.godot/imported/ShantellSans-Bold.ttf-df1f5385501b4ffcb492a1b479c7008f.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/main/ShantellSans-Bold.ttf" 11 | dest_files=["res://.godot/imported/ShantellSans-Bold.ttf-df1f5385501b4ffcb492a1b479c7008f.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiasing=1 17 | generate_mipmaps=true 18 | multichannel_signed_distance_field=false 19 | msdf_pixel_range=8 20 | msdf_size=48 21 | allow_system_fallback=true 22 | force_autohinter=false 23 | hinting=1 24 | subpixel_positioning=1 25 | oversampling=0.0 26 | Fallbacks=null 27 | fallbacks=[] 28 | Compress=null 29 | compress=true 30 | preload=[] 31 | language_support={} 32 | script_support={} 33 | opentype_features={} 34 | -------------------------------------------------------------------------------- /assets/main/TOASTED.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/main/TOASTED.ogg -------------------------------------------------------------------------------- /assets/main/TOASTED.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="oggvorbisstr" 4 | type="AudioStreamOggVorbis" 5 | uid="uid://epn7w26l0klu" 6 | path="res://.godot/imported/TOASTED.ogg-c1e8b6ac7928318b893354d232be317b.oggvorbisstr" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/main/TOASTED.ogg" 11 | dest_files=["res://.godot/imported/TOASTED.ogg-c1e8b6ac7928318b893354d232be317b.oggvorbisstr"] 12 | 13 | [params] 14 | 15 | loop=false 16 | loop_offset=0 17 | bpm=0 18 | beat_count=0 19 | bar_beats=4 20 | -------------------------------------------------------------------------------- /assets/main/mic/mic_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/main/mic/mic_in.png -------------------------------------------------------------------------------- /assets/main/mic/mic_in.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d31jgtyln1dig" 6 | path="res://.godot/imported/mic_in.png-c32df2a61ee8e1b0e30ac320dcf59ce3.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/main/mic/mic_in.png" 14 | dest_files=["res://.godot/imported/mic_in.png-c32df2a61ee8e1b0e30ac320dcf59ce3.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.0 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/main/mic/mic_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/assets/main/mic/mic_out.png -------------------------------------------------------------------------------- /assets/main/mic/mic_out.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://mjuycpvvuxp8" 6 | path="res://.godot/imported/mic_out.png-d297c11e854a23512f9082503524796a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/main/mic/mic_out.png" 14 | dest_files=["res://.godot/imported/mic_out.png-d297c11e854a23512f9082503524796a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.0 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=1 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | CHANNEL_NAME=nevergonnagiveyouup 2 | TEST_USERNAME=rick_astley 3 | -------------------------------------------------------------------------------- /backend/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/.gdignore -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Python env 2 | env/ 3 | .env 4 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Mock backend 2 | 3 | This folder contains the mock backend for the Toaster project. It comes in two flavors: 4 | 5 | - `backend.py` just loops through two sound files with prompt/response over and over for testing basic functionality. 6 | - `backend_twitch.py` connects to the Twitch chat for testing chat commands. 7 | 8 | Communication with the backend follows [this API schema](API_SCHEMA.md). 9 | 10 | ## How to use 11 | 12 | 1. Install [Python 3.11+](https://www.python.org/downloads/) 13 | 2. *(optional)* Create a virtual environment and activate it 14 | 3. Execute this command to install deps: 15 | 16 | ```bash 17 | pip install -r reqs.txt 18 | ``` 19 | 20 | 4. *(for `backend_twitch.py`)* Duplicate `.env.example`, rename it to `.env` and change `CHANNEL_NAME` and `TEST_USERNAME` variables. 21 | 22 | > *The backend will only acknowledge messages from those channel and username.* 23 | 24 | 5. Run one of the following commands to start the backend: 25 | 26 | ```bash 27 | python backend.py 28 | ``` 29 | 30 | ```bash 31 | python backend_twitch.py 32 | ``` 33 | -------------------------------------------------------------------------------- /backend/backend.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import json 4 | import random 5 | import msgpack 6 | 7 | 8 | async def hello(websocket): 9 | backwards = True 10 | 11 | async for message in websocket: 12 | data = json.loads(message) 13 | 14 | if data["type"] == "ReadyForSpeech": 15 | print(">> Ready for speech") 16 | 17 | filename = "chunks/twister_{}.ogg" 18 | prompt = [ 19 | "Hey, Melba! Tell us a tongue twister", 20 | "Hey, Melba! Tell us a tongue twister but backwards", 21 | ] 22 | response = [ 23 | ["Oi, mates.", 1], 24 | ["You wanna hear a tongue twister?", 2], 25 | [ 26 | "Melba's toaster toast toasticular toast that taste as tasty toasty.", 27 | 3, 28 | ], 29 | ["Peace out! Try saying that three times fast.", 4], 30 | ] 31 | 32 | backwards = not backwards 33 | if backwards: 34 | response.reverse() 35 | 36 | chunks = len(response) 37 | for i in range(chunks): 38 | if i == 0: 39 | type = "NewSpeech" 40 | else: 41 | type = "ContinueSpeech" 42 | 43 | current_chunk = response[i] 44 | with open(filename.format(current_chunk[1]), mode="rb") as f: 45 | message = { 46 | "type": type, 47 | "prompt": prompt[backwards], 48 | "response": current_chunk[0], 49 | "emotions": [ 50 | random_emotion(), 51 | random_emotion(), 52 | random_emotion(), 53 | ], 54 | "audio": f.read(), 55 | } 56 | await websocket.send(msgpack.packb(message, use_bin_type=True)) 57 | 58 | await asyncio.sleep(0.5) 59 | await asyncio.sleep(random.uniform(1.0, 2.0)) 60 | 61 | message = {"type": "EndSpeech", "prompt": prompt[backwards]} 62 | await websocket.send(msgpack.packb(message, use_bin_type=True)) 63 | 64 | elif data["type"] == "DoneSpeaking": 65 | print(">>> Done Speaking") 66 | 67 | else: 68 | print(">>> Unknown message") 69 | await websocket.send("hi") 70 | 71 | 72 | async def main(): 73 | print("Ready!") 74 | 75 | async with websockets.serve(hello, "", 9876): 76 | await asyncio.Future() # run forever 77 | 78 | 79 | def random_emotion(): 80 | return random.choice( 81 | [ 82 | "admiration", 83 | "amusement", 84 | "anger", 85 | "annoyance", 86 | "approval", 87 | "caring", 88 | "confusion", 89 | "curiosity", 90 | "desire", 91 | "disappointment", 92 | "disapproval", 93 | "disgust", 94 | "embarrassment", 95 | "excitement", 96 | "fear", 97 | "gratitude", 98 | "grief", 99 | "joy", 100 | "love", 101 | "nervousness", 102 | "optimism", 103 | "pride", 104 | "realization", 105 | "relief", 106 | "remorse", 107 | "sadness", 108 | "surprise", 109 | "neutral", 110 | "anticipation", 111 | ] 112 | ) 113 | 114 | 115 | if __name__ == "__main__": 116 | asyncio.run(main()) 117 | -------------------------------------------------------------------------------- /backend/backend_twitch.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import random 4 | import os 5 | import dotenv 6 | import msgpack 7 | import json 8 | 9 | dotenv.load_dotenv() 10 | 11 | test_username = os.getenv("TEST_USERNAME") 12 | 13 | CONNECTIONS = set() 14 | 15 | 16 | async def backend_server(websocket): 17 | print("--- New client!") 18 | try: 19 | CONNECTIONS.add(websocket) 20 | 21 | async for message in websocket: 22 | data = json.loads(message) 23 | if data["type"] == "ReadyForSpeech": 24 | print(">> Ready for speech") 25 | elif data["type"] == "DoneSpeaking": 26 | print(">> Done speaking") 27 | else: 28 | print(">> Unknown message: ", data) 29 | finally: 30 | CONNECTIONS.discard(websocket) 31 | 32 | 33 | async def message_all(message): 34 | websockets.broadcast(CONNECTIONS, message) 35 | 36 | 37 | async def twitch_client(): 38 | async with websockets.connect("ws://irc-ws.chat.twitch.tv:80") as websocket: 39 | await websocket.send("CAP REQ") 40 | await websocket.send(f"PASS justinfan{random.randint(0, 1000000)}") 41 | await websocket.send(f"NICK justinfan{random.randint(0, 1000000)}") 42 | await websocket.send(f"JOIN #{os.getenv('CHANNEL_NAME', 'melbathetoast')}") 43 | print("--- Twitch connected!") 44 | 45 | async for message in websocket: 46 | if message.startswith("PING"): 47 | await websocket.send("PONG :tmi.twitch.tv") 48 | else: 49 | await twitch_handler(message) 50 | 51 | 52 | async def twitch_handler(message): 53 | if f"PRIVMSG #{test_username} :!toaster" not in message: 54 | return 55 | 56 | message = message.rsplit(" :!toaster", 1)[-1] 57 | message = { 58 | "type": "Command", 59 | "command": message[1:].replace("\r\n", ""), 60 | } 61 | print(f"---- Sending {message}") 62 | await message_all(msgpack.packb(message, use_bin_type=True)) 63 | 64 | 65 | if __name__ == "__main__": 66 | start_server = websockets.serve(backend_server, "", 9876) 67 | 68 | print("Starting server...") 69 | asyncio.get_event_loop().run_until_complete(start_server) 70 | 71 | print("Starting client...") 72 | asyncio.ensure_future(twitch_client()) 73 | 74 | asyncio.get_event_loop().run_forever() 75 | -------------------------------------------------------------------------------- /backend/chunks/twister_1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/chunks/twister_1.ogg -------------------------------------------------------------------------------- /backend/chunks/twister_2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/chunks/twister_2.ogg -------------------------------------------------------------------------------- /backend/chunks/twister_3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/chunks/twister_3.ogg -------------------------------------------------------------------------------- /backend/chunks/twister_4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/chunks/twister_4.ogg -------------------------------------------------------------------------------- /backend/reqs.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/backend/reqs.txt -------------------------------------------------------------------------------- /default_bus_layout.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="AudioBusLayout" load_steps=4 format=3 uid="uid://djao8qcvwmym6"] 2 | 3 | [sub_resource type="AudioEffectLimiter" id="AudioEffectLimiter_1kpja"] 4 | resource_name = "Limiter" 5 | 6 | [sub_resource type="AudioEffectSpectrumAnalyzer" id="AudioEffectSpectrumAnalyzer_gq76t"] 7 | resource_name = "SpectrumAnalyzer" 8 | 9 | [sub_resource type="AudioEffectReverb" id="AudioEffectReverb_0i8b6"] 10 | resource_name = "Reverb" 11 | predelay_msec = 500.0 12 | predelay_feedback = 0.14 13 | room_size = 0.5 14 | damping = 0.46 15 | hipass = 0.68 16 | wet = 0.15 17 | 18 | [resource] 19 | bus/0/effect/0/effect = SubResource("AudioEffectLimiter_1kpja") 20 | bus/0/effect/0/enabled = false 21 | bus/1/name = &"Control" 22 | bus/1/solo = false 23 | bus/1/mute = false 24 | bus/1/bypass_fx = false 25 | bus/1/volume_db = 0.0 26 | bus/1/send = &"Master" 27 | bus/2/name = &"Voice" 28 | bus/2/solo = false 29 | bus/2/mute = false 30 | bus/2/bypass_fx = false 31 | bus/2/volume_db = 0.0 32 | bus/2/send = &"Control" 33 | bus/2/effect/0/effect = SubResource("AudioEffectSpectrumAnalyzer_gq76t") 34 | bus/2/effect/0/enabled = true 35 | bus/2/effect/1/effect = SubResource("AudioEffectReverb_0i8b6") 36 | bus/2/effect/1/enabled = false 37 | bus/3/name = &"Song" 38 | bus/3/solo = false 39 | bus/3/mute = false 40 | bus/3/bypass_fx = false 41 | bus/3/volume_db = -3.0 42 | bus/3/send = &"Master" 43 | -------------------------------------------------------------------------------- /dist/.gdignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !config/ 3 | !songs/ 4 | !run.ps1.example 5 | !.gdignore 6 | !.gitignore 7 | -------------------------------------------------------------------------------- /dist/config/.gdignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/config/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !prod.cfg.example 3 | !.gdignore 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /dist/config/prod.cfg.example: -------------------------------------------------------------------------------- 1 | [OBS] 2 | secure=0 3 | host="127.0.0.1" 4 | port="4455" 5 | password="" 6 | collab=0 7 | 8 | [backend] 9 | secure=0 10 | host="127.0.0.1" 11 | port="9876" 12 | -------------------------------------------------------------------------------- /dist/run.ps1.example: -------------------------------------------------------------------------------- 1 | # Start up script 2 | # Use the actual paths to your executables 3 | 4 | # Plane9 (visualiser) 5 | Start-Process "Plane9.exe" -ArgumentList "-w" -WorkingDirectory "C:\Program Files (x86)\Plane9\" 6 | 7 | # mpv 8 | Start-Process "mpv.exe" -ArgumentList "https://www.youtube.com/watch?v=dQw4w9WgXcQ --volume=50 --ytdl-format=ba --input-ipc-server=\\.\pipe\mpv-pipe" -WorkingDirectory "C:\Program Files\mpv" 9 | Start-Sleep -Seconds 5 10 | 11 | # OBS (streaming) 12 | Start-Process "obs64.exe" -WorkingDirectory "C:\Program Files (x86)\obs-studio\bin\64bit\" 13 | Start-Sleep -Seconds 5 14 | 15 | # Toaster (toaster) 16 | Start-Process ".\toaster.console.exe" -WorkingDirectory "." 17 | -------------------------------------------------------------------------------- /dist/songs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /export_presets.cfg: -------------------------------------------------------------------------------- 1 | [preset.0] 2 | 3 | name="Windows" 4 | platform="Windows Desktop" 5 | runnable=true 6 | advanced_options=true 7 | dedicated_server=false 8 | custom_features="" 9 | export_filter="all_resources" 10 | include_filter="assets/*, licenses/*" 11 | exclude_filter="" 12 | export_path="dist/toaster.exe" 13 | encryption_include_filters="" 14 | encryption_exclude_filters="" 15 | encrypt_pck=false 16 | encrypt_directory=false 17 | script_export_mode=1 18 | 19 | [preset.0.options] 20 | 21 | custom_template/debug="" 22 | custom_template/release="" 23 | debug/export_console_wrapper=2 24 | binary_format/embed_pck=true 25 | texture_format/s3tc_bptc=true 26 | texture_format/etc2_astc=true 27 | binary_format/architecture="x86_64" 28 | codesign/enable=false 29 | codesign/timestamp=true 30 | codesign/timestamp_server_url="" 31 | codesign/digest_algorithm=1 32 | codesign/description="" 33 | codesign/custom_options=PackedStringArray() 34 | application/modify_resources=true 35 | application/icon="" 36 | application/console_wrapper_icon="" 37 | application/icon_interpolation=4 38 | application/file_version="" 39 | application/product_version="" 40 | application/company_name="NOM Network" 41 | application/product_name="Melba Toaster" 42 | application/file_description="Runs the presentation part of Melba Toast, AI VTuber" 43 | application/copyright="2024 NOM Network" 44 | application/trademarks="" 45 | application/export_angle=0 46 | application/export_d3d12=0 47 | application/d3d12_agility_sdk_multiarch=false 48 | ssh_remote_deploy/enabled=false 49 | ssh_remote_deploy/host="user@host_ip" 50 | ssh_remote_deploy/port="22" 51 | ssh_remote_deploy/extra_args_ssh="" 52 | ssh_remote_deploy/extra_args_scp="" 53 | ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' 54 | $action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' 55 | $trigger = New-ScheduledTaskTrigger -Once -At 00:00 56 | $settings = New-ScheduledTaskSettingsSet 57 | $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings 58 | Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true 59 | Start-ScheduledTask -TaskName godot_remote_debug 60 | while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } 61 | Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" 62 | ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue 63 | Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue 64 | Remove-Item -Recurse -Force '{temp_dir}'" 65 | texture_format/bptc=true 66 | texture_format/s3tc=true 67 | texture_format/etc=false 68 | texture_format/etc2=false 69 | -------------------------------------------------------------------------------- /licenses/gd-cubism-license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 MizunagiKB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /licenses/godot-license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). 2 | Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /licenses/shantell-sans-license: -------------------------------------------------------------------------------- 1 | Copyright 2022 The Shantell Sans Project Authors (https://github.com/arrowtype/shantell-sans) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /licenses/spout-gd-license: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /licenses/spout2-license: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020-2024, Lynn Jarvis 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Melba Toaster" 14 | config/version="1.6.0.0" 15 | run/main_scene="res://scenes/main/main.tscn" 16 | config/features=PackedStringArray("4.3", "GL Compatibility") 17 | run/max_fps=60 18 | boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0) 19 | boot_splash/show_image=false 20 | 21 | [audio] 22 | 23 | driver/mix_rate=48000 24 | 25 | [autoload] 26 | 27 | Globals="*res://scripts/shared/globals.gd" 28 | SpeechManager="*res://scripts/main/speech_manager.gd" 29 | CommandManager="*res://scripts/main/command_manager.gd" 30 | 31 | [debug] 32 | 33 | gdscript/warnings/unused_signal=0 34 | gdscript/warnings/unsafe_cast=1 35 | 36 | [display] 37 | 38 | window/size/viewport_width=1920 39 | window/size/viewport_height=1080 40 | window/size/mode=3 41 | window/size/transparent=true 42 | window/subwindows/embed_subwindows=false 43 | window/dpi/allow_hidpi=false 44 | window/per_pixel_transparency/allowed=true 45 | window/vsync/vsync_mode=0 46 | 47 | [editor] 48 | 49 | movie_writer/disable_vsync=true 50 | 51 | [filesystem] 52 | 53 | import/blender/enabled=false 54 | import/fbx/enabled=false 55 | 56 | [input] 57 | 58 | cancel_speech={ 59 | "deadzone": 0.5, 60 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 61 | ] 62 | } 63 | pause_resume={ 64 | "deadzone": 0.5, 65 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194340,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 66 | ] 67 | } 68 | toggle_mute={ 69 | "deadzone": 0.5, 70 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194336,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 71 | ] 72 | } 73 | 74 | [physics] 75 | 76 | 2d/run_on_separate_thread=true 77 | 3d/run_on_separate_thread=true 78 | common/physics_ticks_per_second=120 79 | 80 | [rendering] 81 | 82 | textures/vram_compression/import_etc2_astc=true 83 | viewport/transparent_background=true 84 | viewport/hdr_2d=true 85 | -------------------------------------------------------------------------------- /readme_assets/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/readme_assets/.gdignore -------------------------------------------------------------------------------- /readme_assets/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/readme_assets/interface.png -------------------------------------------------------------------------------- /scenes/live2d/live_2d_melba.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=10 format=3 uid="uid://h1kmx2dkntdj"] 2 | 3 | [ext_resource type="Script" path="res://scripts/live2d/mouth_movement.gd" id="1_2m8bk"] 4 | [ext_resource type="Script" path="res://scripts/live2d/live_2d_melba.gd" id="1_gym56"] 5 | [ext_resource type="Script" path="res://scripts/live2d/eye_blink.gd" id="3_6ft37"] 6 | [ext_resource type="Script" path="res://scripts/live2d/singing_movement.gd" id="4_xiarh"] 7 | [ext_resource type="PackedScene" uid="uid://ds3cr7lote6cs" path="res://scenes/live2d/pinnable_assets.tscn" id="5_rx2gh"] 8 | [ext_resource type="Script" path="res://scripts/live2d/pinnable_assets.gd" id="6_4roha"] 9 | 10 | [sub_resource type="ViewportTexture" id="ViewportTexture_qkp5x"] 11 | viewport_path = NodePath("ModelSprite/Model") 12 | 13 | [sub_resource type="Animation" id="Animation_rc6ug"] 14 | resource_name = "emerge" 15 | length = 10.0 16 | tracks/0/type = "bezier" 17 | tracks/0/imported = false 18 | tracks/0/enabled = true 19 | tracks/0/path = NodePath(".:model_position:x") 20 | tracks/0/interp = 1 21 | tracks/0/loop_wrap = true 22 | tracks/0/keys = { 23 | "handle_modes": PackedInt32Array(0), 24 | "points": PackedFloat32Array(740, -0.25, 0, 0.25, 0), 25 | "times": PackedFloat32Array(0) 26 | } 27 | tracks/1/type = "bezier" 28 | tracks/1/imported = false 29 | tracks/1/enabled = true 30 | tracks/1/path = NodePath(".:model_position:y") 31 | tracks/1/interp = 1 32 | tracks/1/loop_wrap = true 33 | tracks/1/keys = { 34 | "handle_modes": PackedInt32Array(0, 0, 0, 0, 0, 0), 35 | "points": PackedFloat32Array(2080, -0.25, 0, 0.25, 0, 2080, -0.25, 0, 0.6, 0, 1700, -0.675, -1, 0.25, 0, 1700, -0.25, 0, 0.95, 1, 2080, -0.875, -1, 0.25, 0, 1160, -1.13, 0, 0.25, 0), 36 | "times": PackedFloat32Array(0, 2, 4.1, 6.6, 8.5, 10) 37 | } 38 | tracks/2/type = "bezier" 39 | tracks/2/imported = false 40 | tracks/2/enabled = true 41 | tracks/2/path = NodePath(".:model_scale") 42 | tracks/2/interp = 1 43 | tracks/2/loop_wrap = true 44 | tracks/2/keys = { 45 | "handle_modes": PackedInt32Array(0), 46 | "points": PackedFloat32Array(0.33, -0.25, 0, 0.25, 0), 47 | "times": PackedFloat32Array(0) 48 | } 49 | tracks/3/type = "value" 50 | tracks/3/imported = false 51 | tracks/3/enabled = true 52 | tracks/3/path = NodePath(".:model_eyes_target") 53 | tracks/3/interp = 1 54 | tracks/3/loop_wrap = true 55 | tracks/3/keys = { 56 | "times": PackedFloat32Array(0, 3.3, 4.1, 4.8, 6, 6.6, 7.4), 57 | "transitions": PackedFloat32Array(1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5), 58 | "update": 0, 59 | "values": [Vector2(0, 0), Vector2(0, 0), Vector2(-1, 0), Vector2(-1, 0), Vector2(1, 0), Vector2(1, 0), Vector2(0, 0)] 60 | } 61 | 62 | [sub_resource type="AnimationLibrary" id="AnimationLibrary_a8dkn"] 63 | _data = { 64 | "emerge": SubResource("Animation_rc6ug") 65 | } 66 | 67 | [node name="Live2DMelba" type="Node2D"] 68 | script = ExtResource("1_gym56") 69 | model_position = Vector2(740, 2080) 70 | model_scale = 0.33 71 | model_eyes_target = null 72 | metadata/_edit_horizontal_guides_ = [-942.0, -560.0, 1100.0] 73 | metadata/_edit_vertical_guides_ = [1100.0] 74 | 75 | [node name="ModelSprite" type="Sprite2D" parent="."] 76 | unique_name_in_owner = true 77 | z_index = -1 78 | texture_filter = 6 79 | position = Vector2(1100, 1100) 80 | texture = SubResource("ViewportTexture_qkp5x") 81 | centered = false 82 | offset = Vector2(-1100, -1100) 83 | 84 | [node name="Model" type="GDCubismUserModel" parent="ModelSprite"] 85 | assets = "res://assets/live2d/Melba_Final2/melbatoast_model.model3.json" 86 | playback_process_mode = 2 87 | auto_scale = false 88 | adjust_scale = 0.33 89 | mask_viewport_size = Vector2i(1100, 1100) 90 | anim_loop = false 91 | anim_loop_fade_in = true 92 | anim_expression = "Gymbag" 93 | anim_motion = "_0" 94 | ParamAngleX = 0.0 95 | ParamAngleY = 0.0 96 | ParamAngleZ = 0.0 97 | ParamBodyAngleX = 0.0 98 | ParamBodyAngleY = 0.0 99 | ParamBodyAngleZ = 0.0 100 | Param3 = 0.0 101 | Param4 = 0.0 102 | Param5 = 0.0 103 | ParamEyeLOpen = 1.0 104 | ParamEyeLSmile = 0.0 105 | ParamEyeROpen = 1.0 106 | ParamEyeRSmile = 0.0 107 | ParamEyeBallX = 0.0 108 | ParamEyeBallY = 0.0 109 | ParamMouthForm = 0.0 110 | ParamMouthOpenY = 0.0 111 | Param6 = 0.0 112 | Param7 = 0.0 113 | Param8 = 0.0 114 | Param10 = 0.0 115 | Param11 = 0.0 116 | Param15 = 0.0 117 | Param16 = 0.0 118 | Param17 = 0.0 119 | Param9 = 0.0 120 | Param14 = 0.0 121 | Param18 = 0.0 122 | Param21 = 0.0 123 | Param28 = 0.0 124 | Param19 = 0.0 125 | Param20 = 0.0 126 | Param24 = 0.0 127 | Param26 = 0.0 128 | Param25 = 0.0 129 | Param27 = 0.0 130 | ParamBrowLY = 0.0 131 | ParamBrowRY = 0.0 132 | ParamBrowLX = 0.0 133 | ParamBrowRX = 0.0 134 | ParamBrowLAngle = 0.0 135 | ParamBrowRAngle = 0.0 136 | ParamBrowLForm = 0.0 137 | ParamBrowRForm = 0.0 138 | ParamCheek = 0.0 139 | ParamBreath = 0.0 140 | ParamHairFront = 0.0 141 | ParamHairSide = 0.0 142 | ParamHairBack = 0.0 143 | Param = -3.57628e-07 144 | Param2 = 5.96046e-07 145 | Param12 = 0.0 146 | Param13 = 0.0 147 | Param23 = 0.0 148 | Param22 = 0.0 149 | Part35 = 1.0 150 | Part33 = 1.0 151 | Part31 = 1.0 152 | Part29 = 1.0 153 | Model = 1.0 154 | Model5 = 1.0 155 | Model4 = 1.0 156 | Model3 = 1.0 157 | Model2 = 1.0 158 | Head = 1.0 159 | Part4 = 1.0 160 | Part5 = 1.0 161 | Neck = 1.0 162 | Part10 = 1.0 163 | Part11 = 1.0 164 | Gymbag = 1.0 165 | Confuse2 = 1.0 166 | Tears2 = 1.0 167 | Head5 = 1.0 168 | Part22 = 1.0 169 | Part23 = 1.0 170 | Neck2 = 1.0 171 | Part28 = 1.0 172 | Part30 = 1.0 173 | Head4 = 1.0 174 | Head3 = 1.0 175 | Head2 = 1.0 176 | Hair = 1.0 177 | Face = 1.0 178 | Part3 = 1.0 179 | Bread = 1.0 180 | Part17 = 1.0 181 | Body = 1.0 182 | Part12 = 1.0 183 | Part13 = 1.0 184 | Hair2 = 1.0 185 | Face2 = 1.0 186 | Part21 = 1.0 187 | Bread2 = 1.0 188 | Body3 = 1.0 189 | Part19 = 1.0 190 | Part18 = 1.0 191 | Part16 = 1.0 192 | Eyes = 1.0 193 | Mount = 1.0 194 | Body2 = 1.0 195 | Arms = 1.0 196 | Eyes2 = 1.0 197 | Body4 = 1.0 198 | Arms2 = 1.0 199 | Toa = 1.0 200 | Tears = 1.0 201 | Confuse = 1.0 202 | Part6 = 1.0 203 | Part7 = 1.0 204 | Part8 = 1.0 205 | Part9 = 1.0 206 | Part24 = 1.0 207 | Part26 = 1.0 208 | Part27 = 1.0 209 | unique_name_in_owner = true 210 | disable_3d = true 211 | transparent_bg = true 212 | gui_disable_input = true 213 | size = Vector2i(2200, 2200) 214 | render_target_update_mode = 4 215 | 216 | [node name="BreathMovement" type="GDCubismEffectBreath" parent="ModelSprite/Model"] 217 | unique_name_in_owner = true 218 | 219 | [node name="SingingMovement" type="GDCubismEffectCustom" parent="ModelSprite/Model"] 220 | unique_name_in_owner = true 221 | script = ExtResource("4_xiarh") 222 | 223 | [node name="MouthMovement" type="GDCubismEffectCustom" parent="ModelSprite/Model"] 224 | script = ExtResource("1_2m8bk") 225 | 226 | [node name="TargetPoint" type="GDCubismEffectTargetPoint" parent="ModelSprite/Model"] 227 | unique_name_in_owner = true 228 | 229 | [node name="EyeBlinking" type="GDCubismEffectCustom" parent="ModelSprite/Model"] 230 | unique_name_in_owner = true 231 | script = ExtResource("3_6ft37") 232 | 233 | [node name="BlinkTimer" type="Timer" parent="ModelSprite/Model/EyeBlinking"] 234 | unique_name_in_owner = true 235 | wait_time = 3.0 236 | one_shot = true 237 | autostart = true 238 | 239 | [node name="PinnableAssets" type="GDCubismEffectCustom" parent="ModelSprite/Model" node_paths=PackedStringArray("assets")] 240 | script = ExtResource("6_4roha") 241 | assets = NodePath("Assets") 242 | 243 | [node name="Assets" parent="ModelSprite/Model/PinnableAssets" instance=ExtResource("5_rx2gh")] 244 | unique_name_in_owner = true 245 | 246 | [node name="AnimTimer" type="Timer" parent="."] 247 | 248 | [node name="AnimationPlayer" type="AnimationPlayer" parent="."] 249 | libraries = { 250 | "": SubResource("AnimationLibrary_a8dkn") 251 | } 252 | 253 | [connection signal="motion_event" from="ModelSprite/Model" to="." method="_on_gd_cubism_user_model_motion_event"] 254 | -------------------------------------------------------------------------------- /scenes/live2d/pinnable_assets.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=11 format=3 uid="uid://ds3cr7lote6cs"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://cym7l00g0fswe" path="res://assets/live2d/pinnable_assets/glasses.png" id="1_fj5nk"] 4 | [ext_resource type="Texture2D" uid="uid://dow7ry1r7bvdm" path="res://assets/live2d/pinnable_assets/censor.png" id="2_glko1"] 5 | [ext_resource type="Texture2D" uid="uid://q7sygse7kwg5" path="res://assets/live2d/pinnable_assets/hatTop.png" id="3_pc45b"] 6 | [ext_resource type="Texture2D" uid="uid://cyg5rr7kbpiaq" path="res://assets/live2d/pinnable_assets/hatBottom.png" id="4_0yagu"] 7 | [ext_resource type="Texture2D" uid="uid://dqge66kutwpnq" path="res://assets/live2d/pinnable_assets/tetoBand.png" id="5_wm2aa"] 8 | 9 | [sub_resource type="AtlasTexture" id="AtlasTexture_f53nl"] 10 | atlas = ExtResource("2_glko1") 11 | region = Rect2(0, 0, 814, 242) 12 | 13 | [sub_resource type="AtlasTexture" id="AtlasTexture_4icgb"] 14 | atlas = ExtResource("2_glko1") 15 | region = Rect2(814, 0, 814, 242) 16 | 17 | [sub_resource type="AtlasTexture" id="AtlasTexture_0jsom"] 18 | atlas = ExtResource("2_glko1") 19 | region = Rect2(1628, 0, 814, 242) 20 | 21 | [sub_resource type="AtlasTexture" id="AtlasTexture_hove7"] 22 | atlas = ExtResource("2_glko1") 23 | region = Rect2(2442, 0, 814, 242) 24 | 25 | [sub_resource type="SpriteFrames" id="SpriteFrames_uup0d"] 26 | animations = [{ 27 | "frames": [{ 28 | "duration": 1.0, 29 | "texture": SubResource("AtlasTexture_f53nl") 30 | }, { 31 | "duration": 1.0, 32 | "texture": SubResource("AtlasTexture_4icgb") 33 | }, { 34 | "duration": 1.0, 35 | "texture": SubResource("AtlasTexture_0jsom") 36 | }, { 37 | "duration": 1.0, 38 | "texture": SubResource("AtlasTexture_hove7") 39 | }], 40 | "loop": true, 41 | "name": &"default", 42 | "speed": 5.0 43 | }] 44 | 45 | [node name="PinnableAssets" type="Node2D"] 46 | z_index = 100 47 | 48 | [node name="Notes" type="Label" parent="."] 49 | top_level = true 50 | offset_left = 464.0 51 | offset_top = 525.0 52 | offset_right = 2464.0 53 | offset_bottom = 1159.0 54 | theme_override_font_sizes/font_size = 64 55 | text = "- Z Index of the parent node MUST be 100 (above the model) 56 | - for new assets, create a Node2D, then add the sprite there 57 | - for multi-layer nodes, change Z Index of the sprites themselves 58 | 59 | - for multiple assets overlapping, use Node2D's Z Index 60 | - for changing the pivot, use the sprite's pivot and then reset its position 61 | - for changing the rotation, use the sprite's rotation 62 | 63 | Check editor description of the assets/sprites for any overrides" 64 | autowrap_mode = 3 65 | 66 | [node name="Censor" type="Node2D" parent="."] 67 | 68 | [node name="CensorAnimation" type="AnimatedSprite2D" parent="Censor"] 69 | sprite_frames = SubResource("SpriteFrames_uup0d") 70 | autoplay = "default" 71 | frame_progress = 0.928116 72 | 73 | [node name="Glasses" type="Node2D" parent="."] 74 | z_index = -1 75 | 76 | [node name="GlassesSprite" type="Sprite2D" parent="Glasses"] 77 | texture = ExtResource("1_fj5nk") 78 | 79 | [node name="Hat" type="Node2D" parent="."] 80 | 81 | [node name="HatTop" type="Sprite2D" parent="Hat"] 82 | texture = ExtResource("3_pc45b") 83 | 84 | [node name="HatBottom" type="Sprite2D" parent="Hat"] 85 | editor_description = "OVERRIDES: 86 | - Z Index -100" 87 | z_index = -100 88 | texture = ExtResource("4_0yagu") 89 | 90 | [node name="TetoBand" type="Node2D" parent="."] 91 | 92 | [node name="BandAsset" type="Sprite2D" parent="TetoBand"] 93 | editor_description = "OVERRIDES: 94 | - position (70, 30) 95 | - rotation -50 96 | - Z Index -72" 97 | z_index = -72 98 | position = Vector2(70, 30) 99 | rotation = -0.872665 100 | texture = ExtResource("5_wm2aa") 101 | offset = Vector2(-29, -91) 102 | -------------------------------------------------------------------------------- /scenes/main/audio_manager.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bjb7frhypfech"] 2 | 3 | [ext_resource type="Script" path="res://scripts/main/audio_manager.gd" id="1_ybhav"] 4 | [ext_resource type="AudioStream" uid="uid://epn7w26l0klu" path="res://assets/main/TOASTED.ogg" id="2_18nft"] 5 | 6 | [node name="AudioManager" type="Node" node_paths=PackedStringArray("cancel_sound", "speech_player", "song_player")] 7 | script = ExtResource("1_ybhav") 8 | cancel_sound = NodePath("CancelSound") 9 | speech_player = NodePath("SpeechPlayer") 10 | song_player = NodePath("SongPlayer") 11 | 12 | [node name="SpeechPlayer" type="AudioStreamPlayer" parent="."] 13 | bus = &"Voice" 14 | 15 | [node name="SongPlayer" type="AudioStreamPlayer" parent="."] 16 | bus = &"Song" 17 | 18 | [node name="CancelSound" type="AudioStreamPlayer" parent="."] 19 | stream = ExtResource("2_18nft") 20 | bus = &"Voice" 21 | 22 | [connection signal="finished" from="SpeechPlayer" to="." method="_on_speech_player_finished"] 23 | [connection signal="finished" from="SongPlayer" to="." method="_on_song_player_finished"] 24 | -------------------------------------------------------------------------------- /scenes/main/lower_third_manager.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://c61c4c54xgfqi"] 2 | 3 | [ext_resource type="Script" path="res://scripts/main/lower_third.gd" id="1_s6s1h"] 4 | [ext_resource type="FontFile" uid="uid://dljb036lelm30" path="res://assets/main/ShantellSans-Bold.ttf" id="2_tctuy"] 5 | 6 | [sub_resource type="LabelSettings" id="LabelSettings_8wgd5"] 7 | font = ExtResource("2_tctuy") 8 | font_size = 30 9 | outline_size = 5 10 | outline_color = Color(0.556863, 0.384314, 0.117647, 1) 11 | shadow_size = 0 12 | shadow_color = Color(0.388235, 0.262745, 0.0705882, 1) 13 | shadow_offset = Vector2(3, 3) 14 | 15 | [sub_resource type="LabelSettings" id="LabelSettings_rpyvj"] 16 | font = ExtResource("2_tctuy") 17 | font_size = 40 18 | outline_size = 5 19 | outline_color = Color(0.556863, 0.384314, 0.117647, 1) 20 | shadow_size = 0 21 | shadow_color = Color(0.388235, 0.262745, 0.0705882, 1) 22 | shadow_offset = Vector2(3, 3) 23 | 24 | [node name="LowerThirdManager" type="Control"] 25 | layout_mode = 3 26 | anchors_preset = 0 27 | offset_left = 35.0 28 | offset_top = 682.0 29 | offset_right = 1885.0 30 | offset_bottom = 1029.0 31 | script = ExtResource("1_s6s1h") 32 | 33 | [node name="Prompt" type="Label" parent="."] 34 | clip_contents = true 35 | layout_mode = 1 36 | anchors_preset = 10 37 | anchor_right = 1.0 38 | offset_top = 5.0 39 | offset_right = -511.0 40 | offset_bottom = 123.0 41 | grow_horizontal = 2 42 | text = "cjmaxik: тест тест test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string test string" 43 | label_settings = SubResource("LabelSettings_8wgd5") 44 | vertical_alignment = 2 45 | autowrap_mode = 3 46 | text_overrun_behavior = 3 47 | visible_characters_behavior = 1 48 | 49 | [node name="Subtitles" type="Label" parent="."] 50 | layout_mode = 1 51 | anchors_preset = 12 52 | anchor_top = 1.0 53 | anchor_right = 1.0 54 | anchor_bottom = 1.0 55 | offset_top = -205.0 56 | offset_bottom = -2.0 57 | grow_horizontal = 2 58 | grow_vertical = 0 59 | text = "тесттестwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww " 60 | label_settings = SubResource("LabelSettings_rpyvj") 61 | horizontal_alignment = 1 62 | vertical_alignment = 1 63 | autowrap_mode = 3 64 | visible_characters_behavior = 1 65 | 66 | [node name="ClearSubtitlesTimer" type="Timer" parent="."] 67 | wait_time = 5.0 68 | 69 | [node name="PrintTimer" type="Timer" parent="."] 70 | one_shot = true 71 | -------------------------------------------------------------------------------- /scripts/control_panel/helpers.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name CpHelpers 3 | 4 | static var overrides := ["font_color", "font_hover_color", "font_focus_color", "font_pressed_color"] 5 | static var status_overrides := [] 6 | 7 | static func construct_model_control_buttons( 8 | type: String, 9 | parent: Node, 10 | controls: Dictionary, 11 | target_call: Callable 12 | ) -> void: 13 | var callback: Signal 14 | var button_type := Button 15 | 16 | match type: 17 | "animations": 18 | callback = Globals.play_animation 19 | 20 | "pinnable_assets": 21 | callback = Globals.pin_asset 22 | button_type = CheckButton 23 | 24 | "expressions": 25 | callback = Globals.set_expression 26 | 27 | "toggles": 28 | callback = Globals.play_animation 29 | button_type = CheckButton 30 | 31 | for control: String in controls: 32 | var button := button_type.new() 33 | button.text = control 34 | button.name = "%s_%s" % [type.to_pascal_case(), control.to_pascal_case()] 35 | button.focus_mode = Control.FOCUS_NONE 36 | 37 | if type in ["toggles", "pinnable_assets"]: 38 | button.button_pressed = controls[control].enabled 39 | button.pressed.connect(target_call.bind(button)) 40 | else: 41 | button.toggle_mode = true 42 | button.size_flags_horizontal = Control.SIZE_EXPAND_FILL 43 | button.pressed.connect(func() -> void: callback.emit(control)) 44 | 45 | parent.add_child(button) 46 | 47 | static func change_toggle_state( 48 | toggle: Button, 49 | button_pressed: bool, 50 | enabled_text := ">>> STOP <<<", 51 | disabled_text := "Start", 52 | override_color := Color.RED, 53 | apply_color := true 54 | ) -> void: 55 | toggle.set_pressed_no_signal(button_pressed) 56 | toggle.text = enabled_text if button_pressed else disabled_text 57 | 58 | if apply_color: 59 | apply_color_override(toggle, button_pressed, override_color) 60 | 61 | static func apply_color_override( 62 | node: Node, 63 | state: bool, 64 | active_color: Color, 65 | inactive_color: Variant = null, 66 | ) -> void: 67 | for i: String in overrides: 68 | if not state and not inactive_color: 69 | node.remove_theme_color_override(i) 70 | else: 71 | node.add_theme_color_override(i, active_color if state else inactive_color) 72 | 73 | static func change_status_color(node: Button, active: bool) -> void: 74 | node.self_modulate = Color.GREEN if active else Color.RED 75 | 76 | static func array_to_string(arr: Array, separator := " ") -> String: 77 | var s := "" 78 | for i: String in arr: 79 | s += i as String + separator 80 | return s 81 | 82 | static func clear_nodes(nodes: Variant) -> void: 83 | var arr: Array 84 | if typeof(nodes) == TYPE_ARRAY: 85 | arr = nodes 86 | else: 87 | arr.push_back(nodes) 88 | 89 | for node: Node in arr: 90 | for child in node.get_children(): 91 | child.queue_free() 92 | 93 | static func insert_data(node: RichTextLabel, text: String) -> void: 94 | node.clear() 95 | node.append_text(text) 96 | 97 | static func remove_audio_buffer(data: Dictionary) -> Dictionary: 98 | if data is Dictionary: 99 | if data.has("audio"): 100 | data.audio = "<<< TRIMMED >>>" 101 | 102 | return data 103 | -------------------------------------------------------------------------------- /scripts/control_panel/scroll_container.gd: -------------------------------------------------------------------------------- 1 | extends ScrollContainer 2 | var max_scroll_length = 0 3 | @onready var scrollbar = get_v_scroll_bar() 4 | 5 | func _ready(): 6 | scrollbar.changed.connect(handle_scrollbar_changed) 7 | max_scroll_length = scrollbar.max_value 8 | 9 | func handle_scrollbar_changed(): 10 | if max_scroll_length != scrollbar.max_value: 11 | max_scroll_length = scrollbar.max_value 12 | self.scroll_vertical = max_scroll_length 13 | -------------------------------------------------------------------------------- /scripts/live2d/classes/animation.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Live2DAnimation 3 | 4 | var id: int 5 | var duration: float 6 | var ignore_blinking: bool 7 | var override_name: String 8 | var override: Node 9 | 10 | func _init(p_id: int, p_duration: float, p_ignore_blinking := false, p_override_name := "") -> void: 11 | self.id = p_id 12 | self.duration = p_duration 13 | self.ignore_blinking = p_ignore_blinking 14 | self.override_name = p_override_name 15 | -------------------------------------------------------------------------------- /scripts/live2d/classes/pinnable_asset.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name PinnableAsset 3 | 4 | var node_name: String 5 | var enabled: bool 6 | var mesh: String 7 | var position_offset: Vector2 8 | var scale_offset: float 9 | var custom_point: int 10 | var second_point: int 11 | 12 | var node: Node 13 | var initial_points: Array[Vector2] = [Vector2.ZERO, Vector2.ZERO] 14 | 15 | func _init( 16 | p_node_name: String, 17 | p_mesh: String, 18 | p_position_offset: Vector2 = Vector2.ZERO, 19 | p_scale_offset: float = 0.0, 20 | p_custom_point: int = 0, 21 | p_second_point: int = -1 22 | ) -> void: 23 | self.node_name = p_node_name 24 | self.mesh = p_mesh 25 | self.position_offset = p_position_offset 26 | self.scale_offset = p_scale_offset 27 | self.custom_point = p_custom_point 28 | self.second_point = p_custom_point + p_second_point 29 | -------------------------------------------------------------------------------- /scripts/live2d/classes/toggle.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Toggle 3 | 4 | var param: GDCubismParameter 5 | var enabled := false 6 | var default_state := false 7 | var value := 0.0 8 | var id: String 9 | var duration: float 10 | 11 | func _init(p_id: String, p_duration: float, p_enabled := false) -> void: 12 | self.id = p_id 13 | self.duration = p_duration 14 | self.enabled = p_enabled 15 | self.default_state = p_enabled 16 | self.value = 1.0 if enabled else 0.0 17 | -------------------------------------------------------------------------------- /scripts/live2d/eye_blink.gd: -------------------------------------------------------------------------------- 1 | extends GDCubismEffectCustom 2 | 3 | @onready var blink_timer := $BlinkTimer 4 | @export var chance_to_squint := 3.0 5 | 6 | var param_eye_l_open: GDCubismParameter 7 | var param_eye_ball_y: GDCubismParameter 8 | 9 | var squint_value := 0.0 10 | var tween: Tween 11 | 12 | func _ready() -> void: 13 | cubism_init.connect(_on_cubism_init) 14 | 15 | func _on_cubism_init(model: GDCubismUserModel) -> void: 16 | blink_timer.timeout.connect(_on_timer_timeout) 17 | 18 | var param_names := [ 19 | "ParamEyeLOpen", 20 | "ParamEyeBallY", 21 | ] 22 | 23 | for param: GDCubismParameter in model.get_parameters(): 24 | if param_names.has(param.id): 25 | set(param.id.to_snake_case(), param) 26 | 27 | func _process(_delta: float) -> void: 28 | if Globals.last_animation == "sleep": 29 | return 30 | 31 | if Globals.is_singing or not active: 32 | param_eye_l_open.value = 1.0 33 | param_eye_ball_y.value = 0.0 34 | else: 35 | param_eye_l_open.value = squint_value if squint_value > 0.0 else 1.0 36 | param_eye_ball_y.value = -squint_value if squint_value < 0.8 else 0.0 37 | 38 | func _on_timer_timeout() -> void: 39 | if active and Globals.last_animation != "sleep": 40 | if randf_range(0, chance_to_squint) > 1.0 and not Globals.is_singing: 41 | await _squint_tween() 42 | else: 43 | await _blink_tween() 44 | if randi_range(0, 5) == 0: # blink twice 45 | await _blink_tween() 46 | 47 | blink_timer.wait_time = randf_range(1.0, 5.0) 48 | blink_timer.start() 49 | 50 | func _blink_tween() -> void: 51 | if tween: 52 | tween.kill() 53 | 54 | tween = create_tween() 55 | tween.tween_property(self, "squint_value", 0.0, 0.05) 56 | tween.tween_property(param_eye_l_open, "value", 0.0, 0.05) 57 | tween.tween_property(param_eye_l_open, "value", 0.0, randf_range(0.01, 0.1)) 58 | tween.tween_property(param_eye_l_open, "value", 1.0, 0.05) 59 | await tween.finished 60 | 61 | func _squint_tween() -> void: 62 | if tween: 63 | tween.kill() 64 | 65 | squint_value = randf_range(0.5, 1.0) 66 | var time := randf_range(0.1, 0.5) 67 | tween = create_tween().set_parallel().set_trans(Tween.TRANS_SINE) 68 | tween.tween_property(param_eye_l_open, "value", squint_value, time) 69 | tween.tween_property(param_eye_ball_y, "value", -squint_value if squint_value < 0.8 else 0.0, time) 70 | await tween.finished 71 | -------------------------------------------------------------------------------- /scripts/live2d/live_2d_melba.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var sprite: Sprite2D = %ModelSprite 4 | @onready var model: GDCubismUserModel = %Model 5 | @onready var target_point: GDCubismEffectTargetPoint = %TargetPoint 6 | @onready var eye_blink: Node = %EyeBlinking 7 | @onready var anim_timer: Timer = $AnimTimer 8 | 9 | #region TWEENS 10 | @onready var tweens := {} 11 | #endregion 12 | 13 | #region MODEL DATA 14 | @export var model_position: Vector2 = Vector2.ZERO: 15 | get: 16 | if model: 17 | return model.adjust_position + Vector2(model.size) / 2 18 | else: 19 | return Vector2(-200, 580) 20 | 21 | set(value): 22 | if model: 23 | model.adjust_position = value - Vector2(model.size) / 2 24 | 25 | @export var model_scale: float = 1.0: 26 | get: 27 | return model.adjust_scale if model else 1.0 28 | 29 | set(value): 30 | if model: 31 | model.adjust_scale = value 32 | 33 | @export var model_eyes_target: Vector2 = Vector2.ZERO: 34 | get: 35 | return target_point.get_target() if target_point else Vector2.ZERO 36 | 37 | set(value): 38 | if target_point: 39 | target_point.set_target(value) 40 | #endregion 41 | 42 | # Called when the node enters the scene tree for the first time. 43 | func _ready() -> void: 44 | connect_signals() 45 | initialize_animations() 46 | intialize_toggles() 47 | set_expression("end") 48 | play_random_idle_animation() 49 | 50 | func connect_signals() -> void: 51 | Globals.play_animation.connect(play_animation) 52 | Globals.set_expression.connect(set_expression) 53 | Globals.set_toggle.connect(set_toggle) 54 | Globals.nudge_model.connect(nudge_model) 55 | Globals.change_position.connect(_on_change_position) 56 | 57 | anim_timer.timeout.connect(_on_animation_finished) 58 | 59 | func initialize_animations() -> void: 60 | model.playback_process_mode = GDCubismUserModel.IDLE 61 | 62 | for anim: Object in Globals.animations.values(): 63 | if anim.override_name != "": 64 | anim.override = model.get_node(anim.override_name) 65 | 66 | func intialize_toggles() -> void: 67 | var parameters: Array = model.get_parameters() 68 | for param: GDCubismParameter in parameters: 69 | for toggle: Object in Globals.toggles.values(): 70 | if param.get_id() == toggle["id"]: 71 | toggle.param = param 72 | 73 | func _physics_process(_delta: float) -> void: 74 | for toggle: Object in Globals.toggles.values(): 75 | if not toggle.param: 76 | printerr("Cannot found `%s` parameter" % toggle["id"]) 77 | continue 78 | 79 | toggle.param.set_value(toggle.value) 80 | 81 | func _input(event: InputEvent) -> void: 82 | if event as InputEventMouseMotion: 83 | if event.button_mask & MOUSE_BUTTON_MASK_LEFT != 0: 84 | mouse_to_position(event.relative) 85 | 86 | if event.button_mask & MOUSE_BUTTON_MASK_RIGHT != 0: 87 | move_eyes(event.position) 88 | 89 | if event as InputEventMouseButton: 90 | if event.is_pressed(): 91 | match event.button_index: 92 | MOUSE_BUTTON_LEFT: 93 | if event.shift_pressed: 94 | print_model_data() 95 | 96 | MOUSE_BUTTON_WHEEL_UP: 97 | if event.ctrl_pressed: 98 | mouse_to_rotation(Globals.rotation_change) 99 | else: 100 | mouse_to_scale(Globals.scale_change) 101 | 102 | MOUSE_BUTTON_WHEEL_DOWN: 103 | if event.ctrl_pressed: 104 | mouse_to_rotation(-Globals.rotation_change) 105 | else: 106 | mouse_to_scale(-Globals.scale_change) 107 | 108 | MOUSE_BUTTON_MIDDLE: 109 | Globals.change_position.emit(Globals.last_position) 110 | else: 111 | match event.button_index: 112 | MOUSE_BUTTON_RIGHT: 113 | move_eyes(Vector2.ZERO) 114 | 115 | func nudge_model() -> void: 116 | play_random_idle_animation() 117 | 118 | if tweens.has("nudge"): 119 | tweens.nudge.kill() 120 | 121 | tweens.nudge = create_tween() 122 | tweens.nudge.tween_property(model, "speed_scale", 1.7, 1.0).set_ease(Tween.EASE_IN) 123 | tweens.nudge.tween_property(model, "speed_scale", 1.0, 2.0).set_ease(Tween.EASE_OUT) 124 | 125 | func reset_overrides() -> void: 126 | eye_blink.active = true 127 | 128 | func play_animation(anim_name: String) -> void: 129 | reset_overrides() 130 | 131 | Globals.last_animation = anim_name 132 | match anim_name: 133 | "end": 134 | model.stop_motion() 135 | 136 | "random": 137 | play_random_idle_animation() 138 | 139 | anim_name: 140 | if not Globals.animations.has(anim_name): 141 | printerr("Cannot found `%s` animation" % anim_name) 142 | return 143 | 144 | var anim: Object = Globals.animations[anim_name] 145 | 146 | anim_timer.wait_time = anim["duration"] 147 | anim_timer.start() 148 | var anim_id: int = anim.id 149 | var override: Node = anim.override 150 | 151 | eye_blink.active = not anim.ignore_blinking 152 | 153 | if override: 154 | override.active = false 155 | model.start_motion("", anim_id, GDCubismUserModel.PRIORITY_FORCE) 156 | 157 | func set_expression(expression_name: String) -> void: 158 | Globals.last_expression = expression_name 159 | if expression_name == "end": 160 | model.stop_expression() 161 | elif Globals.expressions.has(expression_name): 162 | var expr_id: int = Globals.expressions[expression_name].id 163 | model.start_expression("%s" % expr_id) 164 | 165 | func set_toggle(toggle_name: String, enabled: bool) -> void: 166 | if Globals.toggles.has(toggle_name): 167 | var value_tween: Tween = create_tween() 168 | var toggle: Toggle = Globals.toggles[toggle_name] 169 | 170 | if enabled: 171 | toggle.enabled = true 172 | value_tween.tween_property(toggle, "value", 1.0, toggle.duration) 173 | else: 174 | toggle.enabled = false 175 | value_tween.tween_property(toggle, "value", 0.0, toggle.duration) 176 | 177 | func _on_animation_finished() -> void: 178 | if Globals.last_animation != "end": 179 | play_random_idle_animation() 180 | 181 | func play_random_idle_animation() -> void: 182 | var random_int: int 183 | 184 | if Globals.last_animation == "idle2": 185 | random_int = randi_range(4, 10) 186 | else: 187 | random_int = randi_range(1, 10) 188 | 189 | if random_int <= 3: # 30% chance, 0 % when idle2 was last_animation 190 | play_animation("idle2") 191 | elif random_int <= 6: # 30% chance, ~43% when idle2 was last_animation 192 | play_animation("idle1") 193 | elif random_int <= 10: # 40% chance, ~57% when idle2 was last_animation 194 | play_animation("idle3") 195 | 196 | func get_model_pivot() -> Vector2: 197 | return Vector2(model.size) / 2.0 + model.adjust_position 198 | 199 | func mouse_to_scale(change: float) -> void: 200 | var new_scale: float = model_scale + change 201 | if new_scale < 0.01: 202 | return 203 | 204 | var mouse_pos := get_viewport().get_mouse_position() 205 | var pivot := get_model_pivot() 206 | 207 | var new_pos := model_position - Vector2(-(mouse_pos.x - pivot.x) * change, (mouse_pos.y - pivot.y) * change) / new_scale 208 | 209 | if tweens.has("scale"): 210 | tweens.scale.kill() 211 | 212 | tweens.scale = create_tween() 213 | tweens.scale.set_parallel() 214 | tweens.scale.tween_property(self, "model_scale", new_scale, 0.05) 215 | tweens.scale.tween_property(self, "model_position", new_pos, 0.05) 216 | 217 | func mouse_to_rotation(change: float) -> void: 218 | var pivot: Vector2 = get_model_pivot() 219 | sprite.offset = -pivot 220 | sprite.position = pivot 221 | 222 | var rot: float = sprite.rotation_degrees + change 223 | 224 | if tweens.has("rotation"): 225 | tweens.rotation.kill() 226 | 227 | tweens.rotation = create_tween() 228 | tweens.rotation.tween_property(sprite, "rotation_degrees", rot, 0.1) 229 | await tweens.rotation.finished 230 | 231 | if rot >= 360.0: 232 | rot -= 360.0 233 | sprite.rotation_degrees = rot 234 | elif rot <= -360.0: 235 | rot += 360.0 + change 236 | sprite.rotation_degrees = rot 237 | 238 | func mouse_to_position(change: Vector2) -> void: 239 | var pivot: Vector2 = get_model_pivot() 240 | sprite.offset = -pivot 241 | sprite.position = pivot 242 | model.adjust_position += change 243 | 244 | func move_eyes(mouse_position: Vector2) -> void: 245 | if mouse_position == Vector2.ZERO: 246 | model_eyes_target = Vector2.ZERO 247 | return 248 | 249 | var viewport_size := get_viewport_rect().size 250 | var relative_x: float = (mouse_position.x / viewport_size.x) * 2.0 - 1.0 251 | var relative_y: float = (mouse_position.y / viewport_size.y) * 2.0 - 1.0 252 | model_eyes_target = Vector2(relative_x, -relative_y) 253 | 254 | func _on_change_position(new_position: String) -> void: 255 | new_position = new_position.to_snake_case() 256 | 257 | if not Globals.positions.has(new_position): 258 | return 259 | 260 | var positions: Dictionary = Globals.positions[new_position] 261 | match new_position: 262 | "intro": 263 | _play_emerge_animation() 264 | 265 | _: 266 | if tweens.has("trans"): 267 | tweens.trans.kill() 268 | 269 | var pos: Array = positions.model 270 | var pivot: Vector2 = get_model_pivot() 271 | 272 | tweens.trans = create_tween().set_trans(Tween.TRANS_QUINT) 273 | tweens.trans.set_parallel() 274 | tweens.trans.tween_property(self, "model_position", pos[0], 1) 275 | tweens.trans.tween_property(self, "model_scale", pos[1], 1) 276 | tweens.trans.tween_property(sprite, "rotation", 0, 1) 277 | tweens.trans.tween_property(sprite, "offset", -pivot, 1) 278 | tweens.trans.tween_property(sprite, "position", pivot, 1) 279 | 280 | func _play_emerge_animation() -> void: 281 | $AnimationPlayer.play("emerge") 282 | await $AnimationPlayer.animation_finished 283 | Globals.change_position.emit("default") 284 | Globals.is_paused = false 285 | Globals.ready_for_speech.emit() 286 | 287 | func print_model_data() -> void: 288 | print("Model data: %s, %.2f" % [ 289 | model.adjust_position + Vector2(model.size) / 2, 290 | model.adjust_scale 291 | ]) 292 | -------------------------------------------------------------------------------- /scripts/live2d/mouth_movement.gd: -------------------------------------------------------------------------------- 1 | extends GDCubismEffectCustom 2 | class_name MouthMovement 3 | 4 | @export var mouth_movement := MouthMovement # MouthMovement node 5 | @export var audio_bus_name := "Voice" 6 | @export var min_db := 60.0 7 | @export var min_voice_freq := 450 8 | @export var max_voice_freq := 750 9 | @export var freq_steps := 10 10 | @export var vowel_freqs: Array 11 | 12 | # Parameter Values 13 | @export_category("Param Values") 14 | @export_range(-1.0, 1.0) var max_mouth_value := 0.8 15 | 16 | # Parameters 17 | var param_mouth_open_y: GDCubismParameter 18 | var param_mouth_form: GDCubismParameter 19 | 20 | # For voice analysis 21 | @onready var bus := AudioServer.get_bus_index(audio_bus_name) 22 | @onready var spectrum := AudioServer.get_bus_effect_instance(bus, 0) 23 | var prev_values_amount := 10 24 | var prev_mouth_values := [] 25 | var prev_mouth_form_values := [] 26 | var test_array := [] 27 | var allow_nudge := true 28 | 29 | func _init() -> void: 30 | for i in freq_steps: 31 | vowel_freqs.append([min_voice_freq + i * freq_steps, min_voice_freq + (i + 1) * freq_steps]) 32 | 33 | _reset_values() 34 | 35 | func _ready() -> void: 36 | set_physics_process(false) 37 | 38 | cubism_init.connect(_on_cubism_init) 39 | 40 | Globals.reset_subtitles.connect(_on_reset_subtitles) 41 | 42 | func _on_cubism_init(model: GDCubismUserModel) -> void: 43 | var param_names: Array = [ 44 | "ParamMouthOpenY", 45 | "ParamMouthForm", 46 | ] 47 | 48 | for param: GDCubismParameter in model.get_parameters(): 49 | if param_names.has(param.id): 50 | set(param.id.to_snake_case(), param) 51 | 52 | set_physics_process(true) 53 | 54 | func _on_reset_subtitles() -> void: 55 | _reset_values() 56 | 57 | func _reset_values() -> void: 58 | prev_mouth_values.resize(prev_values_amount) 59 | prev_mouth_values.fill(0.0) 60 | 61 | prev_mouth_form_values.resize(prev_values_amount) 62 | prev_mouth_form_values.fill(0.0) 63 | 64 | test_array.resize(prev_values_amount - 1) 65 | test_array.fill(0.0) 66 | 67 | func _physics_process(_delta: float) -> void: 68 | var overall_magnitude: float = 0.0 69 | var unaltered_mouth_value: float = 0.0 70 | var unaltered_mouth_form: float = 0.0 71 | 72 | if Globals.is_singing or Globals.is_speaking: 73 | overall_magnitude = spectrum.get_magnitude_for_frequency_range( 74 | min_voice_freq, 75 | max_voice_freq 76 | ).length() 77 | var overall_energy: float = clamp((min_db + linear_to_db(overall_magnitude)) / min_db, 0.0, 1.0) 78 | 79 | var selected_mouth_form: float = 0.0 80 | if overall_energy and overall_magnitude: 81 | var max_vowel_enegry := 0.0 82 | var selected_vowel := 0 83 | for i in range(0, vowel_freqs.size()): 84 | var magnitude: float = spectrum.get_magnitude_for_frequency_range( 85 | vowel_freqs[i][0], 86 | vowel_freqs[i][1] 87 | ).length() 88 | var energy: float = clamp((min_db + linear_to_db(magnitude)) / min_db, 0.0, 1.0) 89 | 90 | if energy != 0.0 and energy >= max_vowel_enegry: 91 | max_vowel_enegry = energy 92 | selected_vowel = i 93 | 94 | if selected_vowel > 0: 95 | selected_mouth_form = _map_to_log_range(selected_vowel) 96 | 97 | unaltered_mouth_value = overall_energy * max_mouth_value 98 | unaltered_mouth_form = Globals.current_emotion_modifier + clamp(selected_mouth_form * overall_energy, 0.0, 1.0) 99 | 100 | manage_speaking() 101 | else: 102 | param_mouth_open_y.value = 0.0 103 | param_mouth_form.value = lerp(param_mouth_form.value, Globals.current_emotion_modifier, 0.01) 104 | 105 | prev_mouth_values.remove_at(0) 106 | prev_mouth_values.append(unaltered_mouth_value) 107 | 108 | prev_mouth_form_values.remove_at(0) 109 | prev_mouth_form_values.append(unaltered_mouth_form) 110 | 111 | func manage_speaking() -> void: 112 | # Mouth amplitude 113 | param_mouth_open_y.value = _find_avg(prev_mouth_values.slice(-5)) 114 | 115 | # Mouth form 116 | var mouth_form: float = _find_avg(prev_mouth_form_values.slice(-3)) 117 | param_mouth_form.value = _clamp_to_log_scale(mouth_form) 118 | 119 | if prev_mouth_values[prev_values_amount - 1] != 0.0 \ 120 | and prev_mouth_values.slice(0, prev_values_amount - 1) == test_array \ 121 | and not Globals.is_singing: 122 | Globals.nudge_model.emit() 123 | 124 | func _find_avg(numbers: Array) -> float: 125 | var total := 0.0 126 | for num: float in numbers: 127 | total += num 128 | var avg: float = total / float(numbers.size()) 129 | return avg 130 | 131 | func _clamp_to_log_scale(value: float) -> float: 132 | return 1.0 - (log(1.0 - value + 1.0) / log(1.0 + 1.0)) 133 | 134 | func _map_to_log_range(value: float) -> float: 135 | var old_max: float = clamp(freq_steps - 1, 0, freq_steps - 1) 136 | var new_min: float = 0.0 137 | var new_max: float = 1.0 138 | var epsilon: float = 0.001 139 | 140 | var log_max: float = log(old_max + epsilon) 141 | var log_min: float = log(epsilon) 142 | 143 | var log_value: float = log(value + epsilon) 144 | var normalized_log_value: float = (log_max - log_value) / (log_max - log_min) 145 | 146 | return (normalized_log_value * (new_max - new_min)) + new_min 147 | -------------------------------------------------------------------------------- /scripts/live2d/pinnable_assets.gd: -------------------------------------------------------------------------------- 1 | extends GDCubismEffectCustom 2 | class_name PinnableAssets 3 | 4 | const _90_DEG_IN_RAD = deg_to_rad(90.0) 5 | 6 | #region ASSETS 7 | @export var assets: Node2D 8 | var meshes: Dictionary 9 | var assets_to_pin: Dictionary 10 | var tweens: Dictionary 11 | #endregion 12 | 13 | #region MAIN 14 | func _ready() -> void: 15 | assets.get_node("Notes").visible = false 16 | 17 | _connect_signals() 18 | 19 | func _connect_signals() -> void: 20 | cubism_init.connect(_on_cubism_init) 21 | Globals.pin_asset.connect(_on_pin_asset) 22 | cubism_prologue.connect(_on_cubism_prologue) 23 | #endregion 24 | 25 | #region SIGNALS 26 | func _on_cubism_init(model: GDCubismUserModel) -> void: 27 | meshes = model.get_meshes() 28 | 29 | for asset: PinnableAsset in Globals.pinnable_assets.values(): 30 | asset.node = assets.find_child(asset.node_name) 31 | if not asset.node: 32 | printerr("Cannot found `%s` asset node" % asset.node_name) 33 | continue 34 | 35 | asset.node.modulate.a = 0 36 | 37 | if not meshes.has(asset.mesh): 38 | printerr("Cannot found `%s` mesh" % asset.mesh) 39 | continue 40 | 41 | var ary_mesh: ArrayMesh = meshes[asset.mesh] 42 | var ary_surface: Array = ary_mesh.surface_get_arrays(0) 43 | 44 | asset.initial_points[0] = ary_surface[ArrayMesh.ARRAY_VERTEX][asset.custom_point] 45 | asset.initial_points[1] = ary_surface[ArrayMesh.ARRAY_VERTEX][asset.second_point] 46 | 47 | func _on_pin_asset(node_name: String, enabled: bool) -> void: 48 | if not Globals.pinnable_assets.has(node_name): 49 | printerr("Cannot found `%s` asset node" % node_name) 50 | return 51 | 52 | var asset: PinnableAsset = Globals.pinnable_assets[node_name] 53 | asset.enabled = enabled 54 | 55 | _tween_pinned_asset(asset, enabled) 56 | 57 | func _on_cubism_prologue(model: GDCubismUserModel, _delta: float) -> void: 58 | for asset: String in assets_to_pin.keys(): 59 | _pin(assets_to_pin[asset], model) 60 | #endregion 61 | 62 | #region ASSET FUNCTIONS 63 | 64 | func _tween_pinned_asset(asset: PinnableAsset, enabled: bool) -> void: 65 | var node_name := asset.node_name 66 | 67 | if enabled: 68 | assets_to_pin[node_name] = asset 69 | 70 | if tweens.has(node_name): 71 | tweens[node_name].kill() 72 | 73 | tweens[node_name] = create_tween().set_trans(Tween.TRANS_QUINT) 74 | tweens[node_name].tween_property(asset.node, "modulate:a", 1.0 if enabled else 0.0, 0.5) 75 | 76 | if not enabled: 77 | await tweens[node_name].finished 78 | assets_to_pin.erase(node_name) 79 | 80 | func _pin(asset: PinnableAsset, model: GDCubismUserModel) -> void: 81 | var ary_mesh: ArrayMesh = meshes[asset.mesh] 82 | var ary_surface: Array = ary_mesh.surface_get_arrays(0) 83 | var pos: Vector2 = ary_surface[ArrayMesh.ARRAY_VERTEX][asset.custom_point] 84 | var pos2: Vector2 = ary_surface[ArrayMesh.ARRAY_VERTEX][asset.second_point] 85 | 86 | asset.node.position = pos + (model.adjust_scale * asset.position_offset) 87 | asset.node.scale = Vector2(model.adjust_scale, model.adjust_scale) * asset.scale_offset 88 | asset.node.rotation = _get_asset_rotation(asset.initial_points, [pos, pos2]) 89 | 90 | func _get_asset_rotation(initial_points: Array[Vector2], pos: Array[Vector2]) -> float: 91 | var delta_p: Vector2 = pos[0] - initial_points[0] 92 | var trans_point_b: Vector2 = delta_p + initial_points[1] 93 | 94 | var angle1: float = pos[0].angle_to_point(trans_point_b) 95 | var angle2: float = pos[0].angle_to_point(pos[1]) 96 | 97 | var angle = angle2 - angle1 98 | 99 | if angle > 0: 100 | return angle - _90_DEG_IN_RAD 101 | return angle + _90_DEG_IN_RAD 102 | #endregion 103 | -------------------------------------------------------------------------------- /scripts/live2d/singing_movement.gd: -------------------------------------------------------------------------------- 1 | extends GDCubismEffectCustom 2 | class_name SingingMovement 3 | 4 | @onready var sprite_2d: Sprite2D = %ModelSprite 5 | @onready var gd_cubism_user_model: GDCubismUserModel = %Model 6 | 7 | var bob_interval: float = 0.5 8 | var motion_range: float = 30.0 9 | 10 | # Parameters 11 | ## Head 12 | var param_angle_x: GDCubismParameter 13 | var param_angle_y: GDCubismParameter 14 | var param_angle_z: GDCubismParameter 15 | ## Body 16 | var param_body_angle_x: GDCubismParameter 17 | var param_body_angle_y: GDCubismParameter 18 | var param_body_angle_z: GDCubismParameter 19 | 20 | # Tweens 21 | var tween: Dictionary 22 | var current_motion: Array 23 | 24 | # Timer 25 | @onready var beats_timer := Timer.new() 26 | 27 | func _ready() -> void: 28 | self.cubism_init.connect(_on_cubism_init) 29 | 30 | add_child(beats_timer) 31 | beats_timer.timeout.connect(_on_beats_timer_timeout) 32 | 33 | func _on_cubism_init(model: GDCubismUserModel) -> void: 34 | Globals.start_dancing_motion.connect(_start_motion) 35 | Globals.end_dancing_motion.connect(_end_motion) 36 | 37 | var param_names: Array = [ 38 | "ParamAngleX", 39 | "ParamAngleY", 40 | "ParamAngleZ", 41 | "ParamBodyAngleX", 42 | "ParamBodyAngleY", 43 | "ParamBodyAngleZ" 44 | ] 45 | 46 | for param: GDCubismParameter in model.get_parameters(): 47 | if param_names.has(param.id): 48 | set(param.id.to_snake_case(), param) 49 | 50 | func _start_motion(p_bpm: String) -> void: 51 | var bpm: float = p_bpm as float 52 | Globals.dancing_bpm = bpm 53 | var old_bob_interval: float = bob_interval 54 | bob_interval = motion_range / bpm 55 | 56 | if old_bob_interval != bob_interval: 57 | _stop_motion() 58 | 59 | _start_tween() 60 | _start_timer() 61 | 62 | func _end_motion() -> void: 63 | Globals.dancing_bpm = 0 64 | Globals.play_animation.emit("idle2") 65 | 66 | _stop_motion() 67 | 68 | func _stop_motion() -> void: 69 | _stop_timer() 70 | for axis: String in ["x", "y", "z"]: 71 | _stop(axis) 72 | 73 | func _start_tween() -> void: 74 | current_motion = _random_motion() 75 | 76 | for axis: String in ["x", "y", "z"]: 77 | _dance(axis) if axis in current_motion else _stop(axis) 78 | 79 | func _wait_time() -> float: 80 | return ((60.0 / Globals.dancing_bpm) * 8.0) + Globals.get_audio_compensation() 81 | 82 | func _start_timer() -> void: 83 | beats_timer.wait_time = _wait_time() 84 | beats_timer.start() 85 | 86 | func _stop_timer() -> void: 87 | beats_timer.stop() 88 | 89 | func _on_beats_timer_timeout() -> void: 90 | beats_timer.wait_time = _wait_time() 91 | 92 | var chance := 1.0 if Globals.debug_mode else randf_range(0.0, 1.0) 93 | if randf() < chance: 94 | current_motion = _random_motion() 95 | 96 | if Globals.debug_mode: print("---\n", current_motion) 97 | for axis: String in ["x", "y", "z"]: 98 | _dance(axis) if axis in current_motion else _stop(axis) 99 | 100 | func _dance(axis: String, new_tween := false) -> void: 101 | if tween.has(axis): 102 | if not new_tween and tween[axis].is_running(): 103 | if Globals.debug_mode: print("Skipping: ", axis) 104 | return 105 | 106 | if Globals.debug_mode: print("Running: ", axis) 107 | tween[axis].kill() 108 | 109 | var param := "param_angle_" + axis 110 | var body_param := "param_body_angle_" + axis 111 | 112 | var time: float = bob_interval * _time_modifier(axis) 113 | tween[axis] = create_tween().set_ease(Tween.EASE_IN_OUT).set_parallel().set_loops() 114 | 115 | var random_swing := 1 if randi_range(0, 1) else -1 116 | if Globals.debug_mode: print(axis, ": swing ", random_swing) 117 | 118 | # Swing back 119 | tween[axis].tween_property(get(param), "value", motion_range * random_swing, time) 120 | tween[axis].tween_property(get(body_param), "value", motion_range, time) 121 | 122 | # Swing forth 123 | tween[axis].chain().tween_property(get(param), "value", -motion_range * random_swing, time) 124 | tween[axis].tween_property(get(body_param), "value", -motion_range, time) 125 | 126 | func _stop(axis: String) -> void: 127 | if tween.has(axis): 128 | if Globals.debug_mode: print("Stopping: ", axis) 129 | tween[axis].kill() 130 | 131 | func _time_modifier(axis: String) -> float: 132 | var time_modifier := 1.0 133 | match axis: 134 | "x": 135 | time_modifier = 2.0 136 | "z": 137 | time_modifier = 4.0 138 | _: 139 | time_modifier = 1.0 140 | 141 | return time_modifier 142 | 143 | func _random_motion() -> Array: 144 | if Globals.dancing_bpm <= 100.0: 145 | return [ 146 | ["y", "z"], 147 | ["y", "x"], 148 | ["y"], 149 | ].pick_random() 150 | else: 151 | return [ 152 | ["x", "y", "z"], 153 | ["x", "y"], 154 | ["y", "z"], 155 | ].pick_random() 156 | -------------------------------------------------------------------------------- /scripts/main/audio_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | @onready var voice_bus := AudioServer.get_bus_index("Voice") 4 | 5 | @export var cancel_sound: AudioStreamPlayer 6 | @export var speech_player: AudioStreamPlayer 7 | @export var song_player: AudioStreamPlayer 8 | 9 | var subtitles: Array 10 | 11 | var speech_duration := 0.0 12 | var song_duration := 0.0 13 | 14 | # region PROCESS 15 | 16 | func _ready() -> void: 17 | _connect_signals() 18 | 19 | func _process(_delta: float) -> void: 20 | var full_position: Array[float] = get_position() 21 | var pos: float = full_position[0] 22 | 23 | if subtitles and pos > subtitles[0][0]: 24 | _match_command(subtitles.pop_front()) 25 | 26 | # endregion 27 | 28 | # region SIGNALS 29 | 30 | func _connect_signals() -> void: 31 | Globals.start_speech.connect(_on_start_speech) 32 | 33 | Globals.start_singing.connect(_on_start_singing) 34 | Globals.stop_singing.connect(_on_stop_singing) 35 | 36 | func _on_start_speech() -> void: 37 | if not speech_player.stream: 38 | printerr("No speech stream") 39 | return 40 | 41 | Globals.is_speaking = true 42 | speech_player.play() 43 | 44 | func _on_start_singing(song: Song, seek_time := 0.0) -> void: 45 | prepare_song(song) 46 | subtitles = song.load_subtitles_file() 47 | 48 | play_song(seek_time) 49 | 50 | func _on_stop_singing() -> void: 51 | Globals.is_singing = false 52 | 53 | reset_speech_player() 54 | reset_song_player() 55 | 56 | AudioServer.set_bus_mute(voice_bus, false) 57 | AudioServer.set_bus_effect_enabled(voice_bus, 1, false) 58 | 59 | func _on_speech_player_finished() -> void: 60 | var random_wait := randf_range(0.05, 0.69) 61 | print("-- Waiting for %f seconds" % random_wait) 62 | await get_tree().create_timer(random_wait).timeout 63 | 64 | Globals.is_speaking = false 65 | reset_speech_player() 66 | 67 | Globals.speech_done.emit() 68 | 69 | func _on_song_player_finished() -> void: 70 | subtitles = [] 71 | 72 | Globals.stop_singing.emit() 73 | 74 | # endregion 75 | 76 | func _match_command(line: Array) -> void: 77 | var text: String = line[1] 78 | 79 | if not text.begins_with("&"): 80 | Globals.set_subtitles_fast.emit(text.c_unescape()) 81 | return 82 | 83 | var command: Array = text.split(" ") 84 | match command: 85 | ["&CLEAR"]: 86 | Globals.set_subtitles_fast.emit("") 87 | 88 | ["&START", var bpm]: 89 | Globals.start_dancing_motion.emit(bpm) 90 | 91 | ["&STOP"]: 92 | Globals.end_dancing_motion.emit() 93 | 94 | ["&PIN", var asset_name, var enabled]: 95 | Globals.pin_asset.emit(asset_name, enabled == "1") 96 | 97 | ["&POSITION", var model_position]: 98 | Globals.change_position.emit(model_position) 99 | 100 | ["&TOGGLE", var toggle_name, var enabled]: 101 | Globals.set_toggle.emit(toggle_name, enabled == "1") 102 | 103 | ["&ANIM", var anim_name]: 104 | Globals.play_animation.emit(anim_name) 105 | 106 | ["&FILTER", var source_name, var filter_name, var enabled]: 107 | Globals.toggle_filter.emit(source_name, filter_name, enabled == "1") 108 | 109 | ["&SOURCE", var source_name, var enabled]: 110 | Globals.obs_action.emit("toggle_scene_source", [source_name, enabled == "1"]) 111 | 112 | _: 113 | printerr("SONG: `%s` is not a valid command, ignoring..." % command) 114 | 115 | # region PUBLIC FUNCTIONS 116 | 117 | func play_speech() -> void: 118 | if not speech_player.stream: 119 | printerr("No speech stream") 120 | return 121 | 122 | Globals.is_speaking = true 123 | speech_player.play() 124 | 125 | func play_cancel_sound() -> void: 126 | Globals.is_speaking = true 127 | cancel_sound.play() 128 | await cancel_sound.finished 129 | Globals.is_speaking = false 130 | 131 | func prepare_speech(message: PackedByteArray) -> void: 132 | speech_player.stream = AudioStreamOggVorbis.load_from_buffer(message) 133 | 134 | if not speech_player.stream: 135 | return 136 | 137 | speech_duration = speech_player.stream.get_length() 138 | 139 | func reset_speech_player() -> void: 140 | speech_player.stop() 141 | speech_player.stream = null 142 | speech_duration = 0.0 143 | 144 | func prepare_song(song: Song) -> void: 145 | AudioServer.set_bus_mute(voice_bus, song.mute_voice) 146 | AudioServer.set_bus_effect_enabled(voice_bus, 1, song.reverb) 147 | 148 | song_player.stream = song.load("song") 149 | speech_player.stream = song.load("voice") 150 | 151 | song_duration = song_player.stream.get_length() 152 | 153 | func play_song(seek_time := 0.0) -> void: 154 | Globals.is_singing = true 155 | song_player.play(seek_time) 156 | speech_player.play(seek_time) 157 | 158 | func reset_song_player() -> void: 159 | song_player.stop() 160 | song_player.stream = null 161 | song_duration = 0.0 162 | 163 | func get_position() -> Array[float]: 164 | var comp := Globals.get_audio_compensation() 165 | return [song_player.get_playback_position() + comp, comp] 166 | 167 | func beats_counter_data(full_position: Array[float]) -> Array: 168 | var position := full_position[0] 169 | var beat := position * Globals.dancing_bpm / 60.0 170 | var seconds := position as int 171 | 172 | return [ 173 | seconds / 60.0, 174 | seconds % 60, 175 | position, 176 | full_position[1], 177 | song_duration / 60.0, 178 | song_duration as int % 60, 179 | song_duration, 180 | Globals.dancing_bpm, 181 | beat as int % 4 + 1, 182 | ] 183 | 184 | # endregion 185 | -------------------------------------------------------------------------------- /scripts/main/command_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func execute(command_string: String) -> void: 4 | var command: Array = command_string.split(" ") 5 | 6 | match command: 7 | ["sing", var song_name]: 8 | _sing(song_name.to_lower()) 9 | 10 | ["pause"]: 11 | _pause() 12 | 13 | ["unpause"]: 14 | _unpause() 15 | 16 | ["scene", ..]: 17 | var scene_name: String = command.reduce( 18 | func(acc: String, word: String) -> String: return acc + " " + word, "" 19 | ) 20 | scene_name = scene_name.strip_edges().trim_prefix("scene ") 21 | Globals.change_scene.emit(scene_name) 22 | 23 | _: 24 | print("Unknown command ", command) 25 | 26 | func _pause() -> void: 27 | Globals.is_paused = true 28 | 29 | func _unpause() -> void: 30 | Globals.is_paused = false 31 | 32 | if Globals.is_ready(): 33 | Globals.ready_for_speech.emit() 34 | 35 | func _sing(song_name: String) -> void: 36 | Globals.queue_next_song.emit(song_name, 0) 37 | -------------------------------------------------------------------------------- /scripts/main/lower_third.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | # Controls 4 | @onready var prompt := $Prompt 5 | @onready var subtitles := $Subtitles 6 | @onready var cleanout_timer := $ClearSubtitlesTimer 7 | @onready var print_timer := $PrintTimer 8 | 9 | # Defaults 10 | @onready var default_font_size := { 11 | "Prompt": prompt.label_settings.font_size, 12 | "Subtitles": subtitles.label_settings.font_size, 13 | } 14 | 15 | # Tweens 16 | @onready var tweens := {} 17 | 18 | # Current prompt data 19 | var current_subtitle_text := [] 20 | var current_duration := 0.0 21 | var time_per_symbol := 0.0 22 | 23 | func _ready() -> void: 24 | # Signals 25 | Globals.reset_subtitles.connect(_on_reset_subtitles) 26 | Globals.new_speech.connect(_on_new_speech) 27 | Globals.start_speech.connect(_on_start_speech) 28 | Globals.continue_speech.connect(_on_continue_speech) 29 | Globals.speech_done.connect(_on_speech_done) 30 | Globals.cancel_speech.connect(_on_cancel_speech) 31 | 32 | Globals.ready_for_speech.connect(_on_ready_for_speech) 33 | 34 | cleanout_timer.timeout.connect(_on_clear_subtitles_timer_timeout) 35 | 36 | Globals.reset_subtitles.emit() 37 | 38 | Globals.change_position.connect(_on_change_position) 39 | 40 | Globals.start_singing.connect(_on_start_singing) 41 | Globals.stop_singing.connect(_on_stop_singing) 42 | 43 | Globals.set_subtitles_fast.connect(_on_set_subtitles_fast) 44 | 45 | func _process(delta: float) -> void: 46 | if not print_timer.is_stopped(): 47 | var current_time: float = current_duration + (time_per_symbol * 3) - print_timer.time_left 48 | for token: Array in current_subtitle_text: 49 | var time_check := current_time + delta 50 | if current_subtitle_text[0][1] == "STOP": 51 | print_timer.stop() 52 | return 53 | 54 | if time_check >= current_subtitle_text[0][0]: 55 | var text: String = current_subtitle_text.pop_front()[1] 56 | if not text.length(): 57 | return 58 | 59 | subtitles.text += text 60 | 61 | # Auto toggles 62 | var search_string := text.strip_edges().to_lower() 63 | var unwanted_chars := [".", ",", ":", "?", "!", ";", "-"] 64 | 65 | var result := "" 66 | for c: String in unwanted_chars: 67 | result = search_string.replace(c, "") 68 | 69 | if result.begins_with("toachan"): 70 | if randf() < 0.33: 71 | Globals.set_toggle.emit("toa", true) 72 | 73 | if result.begins_with("toast "): 74 | if randf() < 0.10: 75 | Globals.set_toggle.emit("toast", true) 76 | 77 | subtitles.label_settings.font_size = default_font_size[subtitles.name] 78 | while subtitles.get_line_count() > subtitles.get_visible_line_count(): 79 | subtitles.label_settings.font_size -= 1 80 | 81 | # region SIGNAL CALLBACKS 82 | 83 | func _on_reset_subtitles() -> void: 84 | clear_subtitles() 85 | 86 | func _on_new_speech(_data: Dictionary) -> void: 87 | cleanout_timer.stop() 88 | 89 | func _on_start_speech() -> void: 90 | cleanout_timer.stop() 91 | 92 | func _on_continue_speech(_data: Dictionary) -> void: 93 | cleanout_timer.stop() 94 | 95 | func _on_speech_done() -> void: 96 | _start_clear_subtitles_timer() 97 | 98 | func _on_start_singing(song: Song, seek_time := 0.0) -> void: 99 | clear_subtitles() 100 | cleanout_timer.stop() 101 | 102 | set_prompt(song.full_name, 0.0 if seek_time else song.wait_time) 103 | 104 | func _on_cancel_speech() -> void: 105 | if not Globals.is_speaking: 106 | _start_clear_subtitles_timer() 107 | 108 | func _on_clear_subtitles_timer_timeout() -> void: 109 | clear_subtitles() 110 | 111 | func _on_ready_for_speech() -> void: 112 | if randf() < 0.66: 113 | Globals.set_toggle.emit("toa", false) 114 | 115 | if randf() < 0.66: 116 | Globals.set_toggle.emit("toast", false) 117 | 118 | func _on_set_subtitles_fast(text: String) -> void: 119 | set_subtitles_fast(text) 120 | 121 | # endregion 122 | 123 | # region PUBLIC FUNCTIONS 124 | 125 | func set_prompt(text: String, duration := 0.0) -> void: 126 | prompt.text = "" 127 | text = text.strip_edges() 128 | if not text: 129 | return 130 | 131 | if text == "random": 132 | return 133 | 134 | if text.begins_with("Burnt Melba"): 135 | return 136 | 137 | if text.contains("streamlabs:"): 138 | text = text.replace("streamlabs: ", "") 139 | 140 | prompt.text = text 141 | prompt.label_settings.font_size = default_font_size[prompt.name] 142 | while prompt.get_line_count() > prompt.get_visible_line_count(): 143 | prompt.label_settings.font_size -= 1 144 | 145 | _tween_visible_ratio(prompt, prompt.name, 0.0, 1.0, duration) 146 | 147 | func set_subtitles(text: String, duration := 0.0, continue_print := false) -> void: 148 | if not continue_print: 149 | subtitles.text = "" 150 | print_timer.stop() 151 | 152 | text = text.strip_edges(true) 153 | if not text: 154 | return 155 | 156 | time_per_symbol = (duration - 0.5) / text.length() 157 | 158 | current_duration = duration 159 | current_subtitle_text = [] 160 | 161 | var tokenized_text: Array = text.split(" ") 162 | var time := 0.0 163 | for i in tokenized_text.size(): 164 | time += time_per_symbol * tokenized_text[i].length() 165 | current_subtitle_text.push_back([time, tokenized_text[i] + " "]) 166 | 167 | if i != tokenized_text.size(): 168 | time += time_per_symbol 169 | current_subtitle_text.push_back([time, ""]) 170 | 171 | # Fix last token not appearing at the end 172 | current_subtitle_text[-1] = [duration - time_per_symbol, current_subtitle_text[-1][1]] 173 | current_subtitle_text.push_back([duration, "STOP"]) 174 | 175 | print_timer.start(duration) 176 | 177 | func set_subtitles_fast(text: String) -> void: 178 | subtitles.text = text.strip_edges(false, true) 179 | subtitles.label_settings.font_size = default_font_size["Subtitles"] 180 | 181 | func clear_subtitles() -> void: 182 | cleanout_timer.stop() 183 | 184 | prompt.text = "" 185 | subtitles.text = "" 186 | 187 | prompt.label_settings.font_size = default_font_size["Prompt"] 188 | subtitles.label_settings.font_size = default_font_size["Subtitles"] 189 | 190 | prompt.visible_ratio = 1.0 191 | 192 | current_subtitle_text = [] 193 | current_duration = 0.0 194 | print_timer.stop() 195 | 196 | # endregion 197 | 198 | # region PRIVATE FUNCTIONS 199 | 200 | func _start_clear_subtitles_timer() -> void: 201 | cleanout_timer.wait_time = Globals.time_before_cleanout 202 | cleanout_timer.start() 203 | 204 | func _tween_visible_ratio(label: Label, tween_name: String, start_val: float, final_val: float, duration: float) -> void: 205 | if tweens.has(tween_name): 206 | tweens[tween_name].kill() 207 | 208 | label.visible_ratio = start_val 209 | 210 | tweens[tween_name] = create_tween() 211 | tweens[tween_name].tween_property(label, "visible_ratio", final_val, duration - duration * 0.01) 212 | 213 | func _on_change_position(new_position: String) -> void: 214 | new_position = new_position.to_snake_case() 215 | 216 | if not Globals.positions.has(new_position): 217 | printerr("Position %s does not exist" % new_position) 218 | return 219 | 220 | var positions: Dictionary = Globals.positions[new_position] 221 | 222 | # "model" positions are handled in the model script 223 | match new_position: 224 | "intro": 225 | return 226 | 227 | _: 228 | var pos: Array = positions.lower_third 229 | 230 | if tweens.has("lower_third"): 231 | tweens.lower_third.kill() 232 | 233 | tweens.lower_third = create_tween().set_trans(Tween.TRANS_QUINT) 234 | tweens.lower_third.set_parallel() 235 | tweens.lower_third.tween_property(self, "position", pos[0], 1) 236 | tweens.lower_third.tween_property(self, "scale", pos[1], 1) 237 | 238 | # endregion 239 | 240 | func _on_stop_singing() -> void: 241 | clear_subtitles() 242 | -------------------------------------------------------------------------------- /scripts/main/main.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var model := preload("res://scenes/live2d/live_2d_melba.tscn").instantiate() 4 | var model_sprite: Sprite2D 5 | var user_model: GDCubismUserModel 6 | var model_target_point: GDCubismEffectTargetPoint 7 | 8 | @export var client: WebSocketClient 9 | @export var control_panel: Window 10 | @export var lower_third: Control 11 | @export var mic: AnimatedSprite2D 12 | @export var audio_manager: Node 13 | @export var spout_target: SubViewport 14 | @export var bluescreen: ColorRect 15 | 16 | var connection_attempt: int = 0 17 | 18 | var spout_manager: RefCounted 19 | 20 | func _enter_tree() -> void: 21 | while Globals.config.is_ready == false: 22 | print("Waiting for config...") 23 | await get_tree().create_timer(1.0).timeout 24 | 25 | # region PROCESS 26 | func _ready() -> void: 27 | # Timers 28 | %BeforeNextResponseTimer.wait_time = Globals.time_before_next_response 29 | 30 | _connect_signals() 31 | _add_model() 32 | 33 | await connect_backend() 34 | 35 | func _process(_delta: float) -> void: 36 | if Globals.is_singing: 37 | var full_position: Array[float] = audio_manager.get_position() 38 | 39 | if Globals.show_beats: 40 | $BeatsCounter.text = \ 41 | "TIME: %d:%02d (%6.2f) [%.4f] / %d:%02d (%.2f), BPM: %.1f, BEAT: %d / 4" \ 42 | % audio_manager.beats_counter_data(full_position) 43 | 44 | $BeatsCounter.visible = Globals.show_beats 45 | else: 46 | $BeatsCounter.visible = false 47 | 48 | if spout_manager: 49 | await RenderingServer.frame_post_draw 50 | spout_manager.send_texture() 51 | 52 | func _add_model() -> void: 53 | get_window().size = Vector2i(1920, 1080) 54 | spout_target.add_child(model, true) 55 | 56 | model_sprite = model.get_node("%ModelSprite") 57 | user_model = model.get_node("%Model") 58 | model_target_point = model.get_node("%TargetPoint") 59 | 60 | Globals.change_position.emit(Globals.default_position) 61 | 62 | if OS.has_feature("windows"): 63 | spout_manager = load("res://scripts/main/spout_manager.gd").new() 64 | spout_manager.init(spout_target.get_viewport().get_texture()) 65 | else: 66 | print_debug("Spout not supported on this platform") 67 | 68 | control_panel.get_node("TextureRect").texture = spout_target.get_viewport().get_texture() 69 | 70 | # endregion 71 | 72 | # region SIGNALS 73 | 74 | func _connect_signals() -> void: 75 | Globals.new_speech.connect(_on_new_speech) 76 | Globals.continue_speech.connect(_on_continue_speech) 77 | Globals.cancel_speech.connect(_on_cancel_speech) 78 | Globals.end_speech.connect(_on_end_speech) 79 | 80 | Globals.queue_next_song.connect(_on_queue_next_song) 81 | Globals.start_singing.connect(_on_start_singing) 82 | Globals.stop_singing.connect(_on_stop_singing) 83 | 84 | Globals.toggle_filter.connect(_on_toggle_filter) 85 | Globals.toggle_bluescreen.connect(_on_toggle_bluescreen) 86 | 87 | func _on_ready_for_speech() -> void: 88 | client.send_message({"type": "DoneSpeaking"}) 89 | if not Globals.is_paused: 90 | client.send_message({"type": "ReadyForSpeech"}) 91 | 92 | func _on_data_received(message: PackedByteArray, stats: Array) -> void: 93 | Globals.update_backend_stats.emit(stats) 94 | 95 | var data: Dictionary = MessagePack.decode(message) 96 | if data.error != OK: 97 | printerr("MessagePack decode error: ", data.error) 98 | return 99 | 100 | match data.result.type: 101 | "NewSpeech", "ContinueSpeech", "EndSpeech": 102 | if Globals.is_singing: 103 | printerr("New speech while singing, skipping") 104 | return 105 | 106 | data.result.id = hash(data.result.prompt if data.result.prompt else "MelBuh") 107 | SpeechManager.push_message(data.result) 108 | 109 | "PlayAnimation": 110 | Globals.play_animation.emit(data.result.animationName) 111 | 112 | "SetExpression": 113 | Globals.set_expression.emit(data.result.expressionName) 114 | 115 | "SetToggle": 116 | Globals.set_toggle.emit(data.result.toggleName, data.result.enabled) 117 | 118 | "Command": 119 | CommandManager.execute(data.result.command) 120 | 121 | _: 122 | print(">>> Unhandled data type: ", data) 123 | 124 | func _on_new_speech(data: Dictionary) -> void: 125 | %BeforeNextResponseTimer.stop() 126 | Globals.play_animation.emit("random") 127 | 128 | audio_manager.prepare_speech(data.audio) 129 | 130 | lower_third.set_prompt(data.prompt, 1.0) 131 | lower_third.set_subtitles(data.response, audio_manager.speech_duration) 132 | 133 | audio_manager.play_speech() 134 | 135 | func _on_continue_speech(data: Dictionary) -> void: 136 | %BeforeNextResponseTimer.stop() 137 | 138 | Globals.play_animation.emit("random") 139 | audio_manager.prepare_speech(data.audio) 140 | lower_third.set_subtitles(data.response, audio_manager.speech_duration, true) 141 | audio_manager.play_speech() 142 | await Globals.speech_done 143 | 144 | func _on_cancel_speech() -> void: 145 | %BeforeNextResponseTimer.stop() 146 | 147 | if Globals.is_singing: 148 | return 149 | 150 | lower_third.clear_subtitles() 151 | 152 | audio_manager.reset_speech_player() 153 | lower_third.set_subtitles_fast("[TOASTED]") 154 | 155 | Globals.set_toggle.emit("void", true) 156 | await audio_manager.play_cancel_sound() 157 | Globals.set_toggle.emit("void", false) 158 | 159 | _get_ready_for_next_speech() 160 | 161 | func _on_end_speech() -> void: 162 | _get_ready_for_next_speech() 163 | 164 | func _on_queue_next_song(song_name: String, seek_time: float) -> void: 165 | var next_song: Song 166 | for song in Globals.config.songs: 167 | if song.id.begins_with(song_name): 168 | next_song = song 169 | break 170 | 171 | if not next_song: 172 | print("Could not find song %s" % song_name) 173 | return 174 | 175 | Globals.queued_song = next_song 176 | Globals.queued_song_seek_time = seek_time 177 | 178 | Globals.is_paused = true 179 | if not Globals.is_ready(): 180 | print("Waiting for speech to end...") 181 | await Globals.end_speech 182 | await get_tree().create_timer(2.0).timeout 183 | 184 | if Globals.queued_song: 185 | print("Singing %s..." % song_name) 186 | Globals.start_singing.emit(next_song, seek_time) 187 | else: 188 | print("Song queue is empty") 189 | 190 | func _on_start_singing(_song: Song, _seek_time := 0.0) -> void: 191 | Globals.current_emotion_modifier = 0.0 192 | 193 | # Reset toggles 194 | for toggle: String in Globals.toggles: 195 | Globals.set_toggle.emit(toggle, Globals.toggles[toggle].default_state) 196 | 197 | Globals.is_paused = true 198 | 199 | mic.animation = "in" 200 | mic.play() 201 | 202 | var next_scene := "Collab Song" if Globals.config.get_obs("collab") else "Song" 203 | Globals.change_scene.emit(next_scene) 204 | 205 | func _on_toggle_filter(source_name: String, filter_name: String, enabled: bool) -> void: 206 | var command := { 207 | "sourceName": source_name, 208 | "filterName": filter_name, 209 | "filterEnabled": enabled 210 | } 211 | control_panel.obs.send_command("SetSourceFilterEnabled", command) 212 | 213 | func _on_stop_singing() -> void: 214 | mic.animation = "out" 215 | mic.play() 216 | 217 | $BeatsCounter.visible = false 218 | 219 | Globals.end_dancing_motion.emit() 220 | Globals.end_singing_mouth_movement.emit() 221 | 222 | var next_scene := "Collab" if Globals.config.get_obs("collab") else "Main" 223 | Globals.change_scene.emit(next_scene) 224 | 225 | Globals.queued_song = null 226 | 227 | if client.is_open(): 228 | Globals.is_paused = false 229 | await get_tree().create_timer(2.0).timeout 230 | Globals.ready_for_speech.emit() 231 | 232 | func _on_connection_closed() -> void: 233 | Globals.is_paused = true 234 | control_panel.backend_disconnected() 235 | 236 | var callback: Callable = func(): 237 | if not Globals.is_singing: 238 | Globals.is_paused = false 239 | Globals.ready_for_speech.emit() 240 | 241 | if connection_attempt < 11: 242 | connection_attempt += 1 243 | print("Toaster: Trying to reconnect, attempt %s" % connection_attempt) 244 | await get_tree().create_timer(1.0).timeout 245 | 246 | print("Toaster: Reconnecting...") 247 | connect_backend(callback) 248 | else: 249 | print("Toaster: Too many connection attempts, giving up :(") 250 | connection_attempt = 0 251 | 252 | # endregion 253 | 254 | # region PUBLIC FUNCTIONS 255 | 256 | func connect_backend(callback: Callable = func(): pass ) -> void: 257 | client.connect_client() 258 | await client.connection_established 259 | control_panel.backend_connected() 260 | 261 | if not client.data_received.is_connected(_on_data_received): 262 | client.data_received.connect(_on_data_received) 263 | 264 | if not client.connection_closed.is_connected(_on_connection_closed): 265 | client.connection_closed.connect(_on_connection_closed) 266 | 267 | if not Globals.ready_for_speech.is_connected(_on_ready_for_speech): 268 | Globals.ready_for_speech.connect(_on_ready_for_speech) 269 | 270 | connection_attempt = 0 271 | 272 | if callback: 273 | callback.call() 274 | 275 | func disconnect_backend() -> void: 276 | client.break_connection("from control panel") 277 | await client.connection_closed 278 | 279 | # endregion 280 | 281 | # region PRIVATE FUNCTIONS 282 | 283 | func _get_ready_for_next_speech() -> void: 284 | %BeforeNextResponseTimer.stop() 285 | %BeforeNextResponseTimer.wait_time = Globals.time_before_next_response 286 | %BeforeNextResponseTimer.start() 287 | 288 | func _on_before_next_response_timer_timeout() -> void: 289 | %BeforeNextResponseTimer.stop() 290 | 291 | # Starting the timer again if there are still chunks to skip 292 | if not SpeechManager.ready_for_new_message(): 293 | %BeforeNextResponseTimer.wait_time = Globals.time_before_next_response 294 | %BeforeNextResponseTimer.start() 295 | return 296 | 297 | if not Globals.is_paused: 298 | Globals.ready_for_speech.emit() 299 | 300 | func _on_toggle_bluescreen(toggled_on: bool) -> void: 301 | bluescreen.visible = toggled_on 302 | 303 | # endregion 304 | -------------------------------------------------------------------------------- /scripts/main/speech_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | @export var messages: Array[Dictionary] = [] 4 | var primed := false 5 | var skip_message_id := 0 6 | 7 | var current_speech_id := 0 8 | var current_speech_text := "" 9 | var emotions_array: Array[String] = [] 10 | 11 | func _ready() -> void: 12 | Globals.ready_for_speech.connect(_on_ready_for_speech) 13 | Globals.cancel_speech.connect(_on_cancel_speech) 14 | 15 | func _process(_delta: float) -> void: 16 | if not messages.size() or not Globals.is_ready(): 17 | return 18 | 19 | var message: Dictionary = messages.pop_front() 20 | if not message: 21 | printerr("Message is empty on process") 22 | return 23 | 24 | if message.id == skip_message_id: 25 | printerr("Skipping message %s - skipped on process" % message.id) 26 | return 27 | 28 | current_speech_id = message.id 29 | 30 | if message.has("emotions"): 31 | _process_emotions(message.emotions) 32 | message.emotions = emotions_array 33 | 34 | match message.type: 35 | "NewSpeech": 36 | print("NewSpeech: ", message.id) 37 | skip_message_id = 0 38 | 39 | if message.response == '': 40 | Globals.end_speech.emit() 41 | else: 42 | Globals.new_speech.emit(message) 43 | 44 | "ContinueSpeech": 45 | print("- ContinueSpeech: ", message.id) 46 | Globals.continue_speech.emit(message) 47 | 48 | "EndSpeech": 49 | print("EndSpeech: ", message.id) 50 | print("----------") 51 | current_speech_id = 0 52 | Globals.end_speech.emit() 53 | 54 | _: 55 | printerr("Unknown message type: ", message.type) 56 | return 57 | 58 | func push_message(message: Dictionary) -> void: 59 | if not primed: 60 | if message.type == "EndSpeech": 61 | skip_message_id = 0 62 | current_speech_id = 0 63 | return 64 | elif message.type != "NewSpeech": 65 | printerr("Skipping %s message %s - not primed on push" % [message.type, message.id]) 66 | return 67 | 68 | if message.id == skip_message_id: 69 | printerr("Skipping %s message %s - skipped on push" % [message.type, message.id]) 70 | return 71 | 72 | match message.type: 73 | "NewSpeech": 74 | messages = [] 75 | skip_message_id = 0 76 | primed = true 77 | current_speech_text = message.response 78 | 79 | "ContinueSpeech": 80 | current_speech_text += "\n" + message.response 81 | 82 | "EndSpeech": 83 | current_speech_id = 0 84 | primed = false 85 | current_speech_text += " [END]" 86 | 87 | messages.append(message) 88 | Globals.push_speech_from_queue.emit(current_speech_text, emotions_array) 89 | 90 | func ready_for_new_message() -> bool: 91 | return current_speech_id == 0 and messages.size() == 0 92 | 93 | func _on_ready_for_speech() -> void: 94 | reset_speech() 95 | 96 | func reset_speech() -> void: 97 | current_speech_id = 0 98 | current_speech_text = "" 99 | emotions_array = [] 100 | skip_message_id = 0 101 | 102 | func _on_cancel_speech() -> void: 103 | skip_message_id = current_speech_id 104 | messages = [] 105 | primed = false 106 | 107 | func _process_emotions(emotions: Array) -> void: 108 | if not emotions: 109 | return 110 | 111 | var max_emotion: Array = ["anger", -1.0] 112 | 113 | for emotion: String in emotions: 114 | if not Globals.emotions_modifiers.has(emotion): 115 | printerr("Unknown emotion: %s" % emotion) 116 | return 117 | 118 | if Globals.emotions_modifiers[emotion] > max_emotion[1]: 119 | max_emotion = [emotion, Globals.emotions_modifiers[emotion]] 120 | 121 | Globals.current_emotion_modifier = max_emotion[1] 122 | emotions_array.push_back(max_emotion[0]) 123 | 124 | for toggle: String in ["tears", "void"]: 125 | Globals.set_toggle.emit(toggle, Globals.toggles[toggle].default_state) 126 | 127 | match max_emotion[0]: 128 | "disappointment", "fear", "grief", "sadness": 129 | Globals.set_toggle.emit("tears", true) 130 | 131 | "anger", "disgust", "grief": 132 | Globals.set_toggle.emit("void", true) 133 | -------------------------------------------------------------------------------- /scripts/main/spout_manager.gd: -------------------------------------------------------------------------------- 1 | class_name SpoutManager 2 | 3 | var spout: Spout 4 | var spout_texture: ViewportTexture 5 | 6 | func init(texture: ViewportTexture) -> void: 7 | spout = Spout.new() 8 | spout.set_sender_name("Melba Toaster") 9 | spout_texture = texture 10 | print("Spout2 initialized") 11 | 12 | func send_texture() -> void: 13 | var img := spout_texture.get_image() 14 | spout.send_image(img, img.get_width(), img.get_height(), Spout.FORMAT_RGBA, false) 15 | -------------------------------------------------------------------------------- /scripts/shared/config.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name ToasterConfig 3 | 4 | var config: ConfigFile 5 | var songs: Array[Song] 6 | var is_ready: bool = false 7 | 8 | var SONG_FOLDER_PATH: String 9 | 10 | func _init(debug_mode: bool) -> void: 11 | SONG_FOLDER_PATH = "./dist/songs" if debug_mode else "./songs" 12 | 13 | var config_file: String = "./dist/config/debug.cfg" if debug_mode else "./config/prod.cfg" 14 | _init_config(config_file) 15 | 16 | init_songs(debug_mode) 17 | 18 | func _init_config(config_file: String) -> void: 19 | print("Loading config from %s..." % config_file) 20 | config = _load_config_file(config_file) 21 | is_ready = true 22 | 23 | func init_songs(debug_mode: bool) -> void: 24 | songs = [] 25 | 26 | var dir := DirAccess.open(SONG_FOLDER_PATH) 27 | assert(dir, "There is %s folder in the project root" % SONG_FOLDER_PATH) 28 | 29 | var song_folders := dir.get_directories() 30 | 31 | for id in song_folders: 32 | if id.begins_with("_"): 33 | continue 34 | 35 | var config_path := "%s/%s/config.cfg" % [SONG_FOLDER_PATH, id] 36 | assert(FileAccess.file_exists(config_path), "No config file for %s" % id) 37 | 38 | var config_file: ConfigFile = _load_config_file(config_path) 39 | assert(config_file is ConfigFile, "Config file for %s is corrupted" % id) 40 | 41 | for section in config_file.get_sections(): 42 | var song := {} 43 | for key in config_file.get_section_keys(section): 44 | song[key] = config_file.get_value(section, key) 45 | 46 | if song.has("test") and not debug_mode: 47 | continue 48 | 49 | songs.push_back(Song.new(song, debug_mode)) 50 | 51 | print("Songs loaded: ", songs.size()) 52 | 53 | func _load_config_file(filename: String) -> Variant: 54 | var file: ConfigFile = ConfigFile.new() 55 | 56 | var err: Error = file.load(filename) 57 | assert(err != Error.ERR_FILE_NOT_FOUND, "Cannot find the config file %s" % filename) 58 | 59 | if err != OK: 60 | printerr("Failed to load config file %s, error %d, please check the documentation: https://bit.ly/godot-error" % [filename, err]) 61 | printerr("Close the Toaster.") 62 | return null 63 | 64 | return file 65 | 66 | func get_obs(key: String) -> Variant: 67 | return config.get_value("OBS", key, "") 68 | 69 | func get_backend(key: String) -> Variant: 70 | return config.get_value("backend", key) 71 | -------------------------------------------------------------------------------- /scripts/shared/globals.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # region EVENT BUS 4 | 5 | signal play_animation(anim_name: String) 6 | signal set_expression(expression_name: String, enabled: bool) 7 | signal set_toggle(toggle_name: String, enabled: bool) 8 | signal pin_asset(asset_name: String, enabled: bool) 9 | 10 | signal queue_next_song(song_name: String, seek_time: float) 11 | signal start_singing(song: Song, seek_time: float) 12 | signal stop_singing() 13 | 14 | signal start_dancing_motion(bpm: String) 15 | signal end_dancing_motion() 16 | signal start_singing_mouth_movement() 17 | signal end_singing_mouth_movement() 18 | signal nudge_model() 19 | 20 | signal change_position(name: String) 21 | signal change_scene(scene: String) 22 | 23 | # Speech Manager 24 | signal ready_for_speech() 25 | signal new_speech(data: Dictionary) 26 | signal start_speech() 27 | signal push_speech_from_queue(response: String, emotions: Array[String]) 28 | signal continue_speech(data: Dictionary) 29 | signal end_speech(data: Dictionary) 30 | signal speech_done() 31 | signal cancel_speech() 32 | signal reset_subtitles() 33 | signal set_subtitles(text: String, duration: float, continue_print: bool) 34 | signal set_subtitles_fast(text: String) 35 | 36 | signal toggle_filter(source_name: String, filter_name: String, enabled: bool) 37 | signal toggle_bluescreen(toggled_on: bool) 38 | 39 | signal update_backend_stats(data: Array) 40 | 41 | # OBS Websocket 42 | signal obs_action(action: String, args: String) 43 | 44 | # endregion 45 | 46 | # region MELBA STATE 47 | 48 | @export var debug_mode := OS.is_debug_build() 49 | @export var config := ToasterConfig.new(debug_mode) 50 | 51 | @export var is_paused := true 52 | @export var is_speaking := false 53 | @export var is_singing := false 54 | @export var dancing_bpm := 0.0 55 | 56 | @export var show_beats := debug_mode 57 | 58 | @export var scene_override := false 59 | @export var scene_override_to := "Stay" 60 | 61 | @export var position_override := false 62 | @export var position_override_to := default_position 63 | 64 | @export var queued_song: Song 65 | @export var queued_song_seek_time := 0.0 66 | 67 | @export var time_before_cleanout := 10.0 68 | @export var time_before_next_response := 0.5 69 | 70 | func is_ready() -> bool: 71 | return not (is_speaking or is_singing) 72 | 73 | # endregion 74 | 75 | # region SCENE DATA 76 | 77 | static var default_position := "default" 78 | static var last_position := default_position 79 | static var positions := Variables.positions 80 | static var scale_change := 0.05 81 | static var rotation_change := 5.0 82 | 83 | # region LIVE2D DATA 84 | 85 | static var pinnable_assets := Variables.pinnable_assets 86 | static var toggles := Variables.toggles 87 | static var animations := Variables.animations 88 | static var last_animation := "" 89 | 90 | static var expressions := { 91 | "end": {"id": "none"} 92 | } 93 | static var last_expression := "" 94 | 95 | static var emotions_modifiers := Variables.emotions_modifiers 96 | static var current_emotion_modifier := 0.0 97 | 98 | # endregion 99 | 100 | # region HELPERS 101 | 102 | func get_audio_compensation() -> float: 103 | return AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency() + (1 / Engine.get_frames_per_second()) * 2 104 | 105 | # endregion 106 | 107 | # region EVENT BUS DEBUG 108 | 109 | func _ready() -> void: 110 | for s in get_signal_list(): 111 | self.connect(s.name, _debug_event.bind(s.name)) 112 | 113 | self.change_position.connect(_on_change_position) 114 | 115 | func _on_change_position(position: String) -> void: 116 | self.last_position = position 117 | 118 | func _debug_event(arg1: Variant, arg2: Variant = null, arg3: Variant = null, arg4: Variant = null, arg5: Variant = null) -> void: 119 | if not debug_mode: 120 | return 121 | 122 | var args := [arg1, arg2, arg3, arg4, arg5].filter(func(d: Variant) -> bool: return d != null) 123 | 124 | var eventName: String = args.pop_back() 125 | 126 | # remove audio buffer from debug 127 | if args.size() > 0: 128 | if eventName in ["new_speech", "continue_speech", "end_speech"]: 129 | args[0] = CpHelpers.remove_audio_buffer(args[0].duplicate()) 130 | 131 | print_debug( 132 | "EVENT BUS: `%s` - %s" % [eventName, args] 133 | ) 134 | 135 | # endregion 136 | -------------------------------------------------------------------------------- /scripts/shared/messagepack.gd: -------------------------------------------------------------------------------- 1 | # based on https://github.com/xtpor/godot-msgpack/blob/master/msgpack.gd 2 | 3 | extends Node 4 | class_name MessagePack 5 | 6 | # region DECODE 7 | 8 | static func decode(bytes: PackedByteArray) -> Dictionary: 9 | var buffer = StreamPeerBuffer.new() 10 | buffer.big_endian = true 11 | buffer.data_array = bytes 12 | 13 | var ctx = {error = OK, error_string = ""} 14 | var value = _decode(buffer, ctx) 15 | if ctx.error == OK: 16 | if buffer.get_position() == buffer.get_size(): 17 | return {result = value, error = OK, error_string = ""} 18 | else: 19 | var msg = "excess buffer %s bytes" % [buffer.get_size() - buffer.get_position()] 20 | return {result = null, error = FAILED, error_string = msg} 21 | else: 22 | return {result = null, error = ctx.error, error_string = ctx.error_string} 23 | 24 | static func _decode(buffer, ctx): 25 | if buffer.get_position() == buffer.get_size(): 26 | ctx.error = FAILED 27 | ctx.error_string = "unexpected end of input" 28 | return null 29 | 30 | var head = buffer.get_u8() 31 | if head == 0xc0: 32 | return null 33 | elif head == 0xc2: 34 | return false 35 | elif head == 0xc3: 36 | return true 37 | 38 | # Integers 39 | elif head & 0x80 == 0: 40 | # positive fixnum 41 | return head 42 | elif (~head) & 0xe0 == 0: 43 | # negative fixnum 44 | return head - 256 45 | elif head == 0xcc: 46 | # uint 8 47 | if buffer.get_size() - buffer.get_position() < 1: 48 | ctx.error = FAILED 49 | ctx.error_string = "not enough buffer for uint8" 50 | return null 51 | 52 | return buffer.get_u8() 53 | elif head == 0xcd: 54 | # uint 16 55 | if buffer.get_size() - buffer.get_position() < 2: 56 | ctx.error = FAILED 57 | ctx.error_string = "not enough buffer for uint16" 58 | return null 59 | 60 | return buffer.get_u16() 61 | elif head == 0xce: 62 | # uint 32 63 | if buffer.get_size() - buffer.get_position() < 4: 64 | ctx.error = FAILED 65 | ctx.error_string = "not enough buffer for uint32" 66 | return null 67 | 68 | return buffer.get_u32() 69 | elif head == 0xcf: 70 | # uint 64 71 | if buffer.get_size() - buffer.get_position() < 8: 72 | ctx.error = FAILED 73 | ctx.error_string = "not enough buffer for uint64" 74 | return null 75 | 76 | return buffer.get_u64() 77 | elif head == 0xd0: 78 | # int 8 79 | if buffer.get_size() - buffer.get_position() < 1: 80 | ctx.error = FAILED 81 | ctx.error_string = "not enogh buffer for int8" 82 | return null 83 | 84 | return buffer.get_8() 85 | elif head == 0xd1: 86 | # int 16 87 | if buffer.get_size() - buffer.get_position() < 2: 88 | ctx.error = FAILED 89 | ctx.error_string = "not enogh buffer for int16" 90 | return null 91 | 92 | return buffer.get_16() 93 | elif head == 0xd2: 94 | # int 32 95 | if buffer.get_size() - buffer.get_position() < 4: 96 | ctx.error = FAILED 97 | ctx.error_string = "not enough buffer for int32" 98 | return null 99 | 100 | return buffer.get_32() 101 | elif head == 0xd3: 102 | # int 64 103 | if buffer.get_size() - buffer.get_position() < 8: 104 | ctx.error = FAILED 105 | ctx.error_string = "not enough buffer for int64" 106 | return null 107 | 108 | return buffer.get_64() 109 | 110 | # Float 111 | elif head == 0xca: 112 | # float32 113 | if buffer.get_size() - buffer.get_position() < 4: 114 | ctx.error = FAILED 115 | ctx.error_string = "not enough buffer for float32" 116 | return null 117 | 118 | return buffer.get_float() 119 | elif head == 0xcb: 120 | # float64 121 | if buffer.get_size() - buffer.get_position() < 4: 122 | ctx.error = FAILED 123 | ctx.error_string = "not enough buffer for float64" 124 | return null 125 | 126 | return buffer.get_double() 127 | 128 | # String 129 | elif (~head) & 0xa0 == 0: 130 | var size = head & 0x1f 131 | if buffer.get_size() - buffer.get_position() < size: 132 | ctx.error = FAILED 133 | ctx.error_string = "not enough buffer for fixstr required %s bytes" % [size] 134 | return null 135 | 136 | return buffer.get_utf8_string(size) 137 | elif head == 0xd9: 138 | if buffer.get_size() - buffer.get_position() < 1: 139 | ctx.error = FAILED 140 | ctx.error_string = "not enough buffer for str8 size" 141 | return null 142 | 143 | var size = buffer.get_u8() 144 | if buffer.get_size() - buffer.get_position() < size: 145 | ctx.error = FAILED 146 | ctx.error_string = "not enough buffer for str8 data required %s bytes" % [size] 147 | return null 148 | 149 | return buffer.get_utf8_string(size) 150 | elif head == 0xda: 151 | if buffer.get_size() - buffer.get_position() < 2: 152 | ctx.error = FAILED 153 | ctx.error_string = "not enough buffer for str16 size" 154 | return null 155 | 156 | var size = buffer.get_u16() 157 | if buffer.get_size() - buffer.get_position() < size: 158 | ctx.error = FAILED 159 | ctx.error_string = "not enough buffer for str16 data required %s bytes" % [size] 160 | return null 161 | 162 | return buffer.get_utf8_string(size) 163 | elif head == 0xdb: 164 | if buffer.get_size() - buffer.get_position() < 4: 165 | ctx.error = FAILED 166 | ctx.error_string = "not enough buffer for str32 size" 167 | return null 168 | 169 | var size = buffer.get_u32() 170 | if buffer.get_size() - buffer.get_position() < size: 171 | ctx.error = FAILED 172 | ctx.error_string = "not enough buffer for str32 data required %s bytes" % [size] 173 | return null 174 | 175 | return buffer.get_utf8_string(size) 176 | 177 | # Binary 178 | elif head == 0xc4: 179 | if buffer.get_size() - buffer.get_position() < 1: 180 | ctx.error = FAILED 181 | ctx.error_string = "not enough buffer for bin8 size" 182 | return null 183 | 184 | var size = buffer.get_u8() 185 | if buffer.get_size() - buffer.get_position() < size: 186 | ctx.error = FAILED 187 | ctx.error_string = "not enough buffer for bin8 data required %s bytes" % [size] 188 | return null 189 | 190 | var res = buffer.get_data(size) 191 | assert(res[0] == OK) 192 | return res[1] 193 | elif head == 0xc5: 194 | if buffer.get_size() - buffer.get_position() < 2: 195 | ctx.error = FAILED 196 | ctx.error_string = "not enough buffer for bin16 size" 197 | return null 198 | 199 | var size = buffer.get_u16() 200 | if buffer.get_size() - buffer.get_position() < size: 201 | ctx.error = FAILED 202 | ctx.error_string = "not enough buffer for bin16 data required %s bytes" % [size] 203 | return null 204 | 205 | var res = buffer.get_data(size) 206 | assert(res[0] == OK) 207 | return res[1] 208 | elif head == 0xc6: 209 | if buffer.get_size() - buffer.get_position() < 4: 210 | ctx.error = FAILED 211 | ctx.error_string = "not enough buffer for bin32 size" 212 | return null 213 | 214 | var size = buffer.get_u32() 215 | if buffer.get_size() - buffer.get_position() < size: 216 | ctx.error = FAILED 217 | ctx.error_string = "not enough buffer for bin32 data required %s bytes" % [size] 218 | return null 219 | 220 | var res = buffer.get_data(size) 221 | assert(res[0] == OK) 222 | return res[1] 223 | 224 | # Array 225 | elif head & 0xf0 == 0x90: 226 | var size = head & 0x0f 227 | var res = [] 228 | for i in range(size): 229 | res.append(_decode(buffer, ctx)) 230 | if ctx.error != OK: 231 | return null 232 | return res 233 | elif head == 0xdc: 234 | if buffer.get_size() - buffer.get_position() < 2: 235 | ctx.error = FAILED 236 | ctx.error_string = "not enough buffer for array16 size" 237 | return null 238 | 239 | var size = buffer.get_u16() 240 | var res = [] 241 | for i in range(size): 242 | res.append(_decode(buffer, ctx)) 243 | if ctx.error != OK: 244 | return null 245 | return res 246 | elif head == 0xdd: 247 | if buffer.get_size() - buffer.get_position() < 4: 248 | ctx.error = FAILED 249 | ctx.error_string = "not enough buffer for array32 size" 250 | return null 251 | 252 | var size = buffer.get_u32() 253 | var res = [] 254 | for i in range(size): 255 | res.append(_decode(buffer, ctx)) 256 | if ctx.error != OK: 257 | return null 258 | return res 259 | 260 | # Map 261 | elif head & 0xf0 == 0x80: 262 | var size = head & 0x0f 263 | var res = {} 264 | for i in range(size): 265 | var k = _decode(buffer, ctx) 266 | if ctx.error != OK: 267 | return null 268 | 269 | var v = _decode(buffer, ctx) 270 | if ctx.error != OK: 271 | return null 272 | 273 | res[k] = v 274 | return res 275 | elif head == 0xde: 276 | if buffer.get_size() - buffer.get_position() < 2: 277 | ctx.error = FAILED 278 | ctx.error_string = "not enough buffer for map16 size" 279 | return null 280 | 281 | var size = buffer.get_u16() 282 | var res = {} 283 | for i in range(size): 284 | var k = _decode(buffer, ctx) 285 | if ctx.error != OK: 286 | return null 287 | 288 | var v = _decode(buffer, ctx) 289 | if ctx.error != OK: 290 | return null 291 | 292 | res[k] = v 293 | return res 294 | elif head == 0xdf: 295 | if buffer.get_size() - buffer.get_position() < 4: 296 | ctx.error = FAILED 297 | ctx.error_string = "not enough buffer for map32 size" 298 | return null 299 | 300 | var size = buffer.get_u32() 301 | var res = {} 302 | for i in range(size): 303 | var k = _decode(buffer, ctx) 304 | if ctx.error != OK: 305 | return null 306 | 307 | var v = _decode(buffer, ctx) 308 | if ctx.error != OK: 309 | return null 310 | 311 | res[k] = v 312 | return res 313 | 314 | else: 315 | ctx.error = FAILED 316 | ctx.error_string = "invalid byte tag %02X at pos %s" % [head, buffer.get_position()] 317 | return null 318 | 319 | # endregion 320 | 321 | # region ENCODE 322 | 323 | static func encode(value: Variant) -> Dictionary: 324 | var ctx = {error = OK, error_string = ""} 325 | var buffer = StreamPeerBuffer.new() 326 | buffer.big_endian = true 327 | 328 | _encode(buffer, value, ctx) 329 | if ctx.error == OK: 330 | return { 331 | result = buffer.data_array, 332 | error = OK, 333 | error_string = "", 334 | } 335 | else: 336 | return { 337 | result = PackedByteArray(), 338 | error = ctx.error, 339 | error_string = ctx.error_string, 340 | } 341 | 342 | static func _encode(buf, value, ctx): 343 | match typeof(value): 344 | TYPE_NIL: 345 | buf.put_u8(0xc0) 346 | 347 | TYPE_BOOL: 348 | if value: 349 | buf.put_u8(0xc3) 350 | else: 351 | buf.put_u8(0xc2) 352 | 353 | TYPE_INT: 354 | if -(1 << 5) <= value and value <= (1 << 7) - 1: 355 | # fixnum (positive and negative) 356 | buf.put_8(value) 357 | elif -(1 << 7) <= value and value <= (1 << 7): 358 | buf.put_u8(0xd0) 359 | buf.put_8(value) 360 | elif -(1 << 15) <= value and value <= (1 << 15): 361 | buf.put_u8(0xd1) 362 | buf.put_16(value) 363 | elif -(1 << 31) <= value and value <= (1 << 31): 364 | buf.put_u8(0xd2) 365 | buf.put_32(value) 366 | else: 367 | buf.put_u8(0xd3) 368 | buf.put_64(value) 369 | 370 | TYPE_FLOAT: 371 | buf.put_u8(0xca) 372 | buf.put_float(value) 373 | 374 | TYPE_STRING: 375 | var bytes = value.to_utf8() 376 | 377 | var size = bytes.size() 378 | if size <= (1 << 5) - 1: 379 | # type fixstr [101XXXXX] 380 | buf.put_u8(0xa0 | size) 381 | elif size <= (1 << 8) - 1: 382 | # type str 8 383 | buf.put_u8(0xd9) 384 | buf.put_u8(size) 385 | elif size <= (1 << 16) - 1: 386 | # type str 16 387 | buf.put_u8(0xda) 388 | buf.put_u16(size) 389 | elif size <= (1 << 32) - 1: 390 | # type str 32 391 | buf.put_u8(0xdb) 392 | buf.put_u32(size) 393 | else: 394 | assert(false) 395 | 396 | buf.put_data(bytes) 397 | 398 | TYPE_PACKED_BYTE_ARRAY: 399 | var size = value.size() 400 | if size <= (1 << 8) - 1: 401 | buf.put_u8(0xc4) 402 | buf.put_u8(size) 403 | elif size <= (1 << 16) - 1: 404 | buf.put_u8(0xc5) 405 | buf.put_u16(size) 406 | elif size <= (1 << 32) - 1: 407 | buf.put_u8(0xc6) 408 | buf.put_u32(size) 409 | else: 410 | assert(false) 411 | 412 | buf.put_data(value) 413 | 414 | TYPE_ARRAY: 415 | var size = value.size() 416 | if size <= 15: 417 | # type fixarray [1001XXXX] 418 | buf.put_u8(0x90 | size) 419 | elif size <= (1 << 16) - 1: 420 | # type array 16 421 | buf.put_u8(0xdc) 422 | buf.put_u16(size) 423 | elif size <= (1 << 32) - 1: 424 | # type array 32 425 | buf.put_u8(0xdd) 426 | buf.put_u32(size) 427 | else: 428 | assert(false) 429 | 430 | for obj in value: 431 | _encode(buf, obj, ctx) 432 | if ctx.error != OK: 433 | return 434 | 435 | TYPE_DICTIONARY: 436 | var size = value.size() 437 | if size <= 15: 438 | # type fixmap [1000XXXX] 439 | buf.put_u8(0x80 | size) 440 | elif size <= (1 << 16) - 1: 441 | # type map 16 442 | buf.put_u8(0xde) 443 | buf.put_u16(size) 444 | elif size <= (1 << 32) - 1: 445 | # type map 32 446 | buf.put_u8(0xdf) 447 | buf.put_u32(size) 448 | else: 449 | assert(false) 450 | 451 | for key in value: 452 | _encode(buf, key, ctx) 453 | if ctx.error != OK: 454 | return 455 | 456 | _encode(buf, value[key], ctx) 457 | if ctx.error != OK: 458 | return 459 | _: 460 | ctx.error = FAILED 461 | ctx.error_string = "unsupported data type %s" % [typeof(value)] 462 | 463 | # endregion 464 | -------------------------------------------------------------------------------- /scripts/shared/song.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Song 3 | 4 | var FOLDER_PATH: String 5 | 6 | var id: String 7 | var path: String 8 | var song_name: String 9 | var full_name: String 10 | var short_name: String 11 | var wait_time: float 12 | var mute_voice: bool = true 13 | var reverb: bool = false 14 | 15 | func _init( 16 | p_data: Dictionary, 17 | debug_mode: bool 18 | ) -> void: 19 | FOLDER_PATH = './dist/songs/%s/%s' if debug_mode else './songs/%s/%s' 20 | 21 | self.id = p_data.id 22 | self.song_name = p_data.name 23 | self.wait_time = p_data.wait_time 24 | 25 | if p_data.has("mute_voice"): 26 | self.mute_voice = p_data.mute_voice 27 | 28 | if p_data.has("reverb"): 29 | self.reverb = p_data.reverb 30 | 31 | if p_data.has("artist"): 32 | self.full_name = "%s - %s" % [p_data.artist, p_data.name] 33 | else: 34 | self.full_name = p_data.name 35 | 36 | if p_data.has("feat"): 37 | self.full_name += " %s" % p_data.feat 38 | 39 | if p_data.has("short_name"): 40 | self.short_name = p_data.short_name 41 | 42 | self.path = FOLDER_PATH % [p_data.id, "%s.ogg"] 43 | 44 | assert( 45 | FileAccess.file_exists(self.path % "song") and FileAccess.file_exists(self.path % "voice"), 46 | "SONG: Files for %s are not found!" % self.id 47 | ) 48 | 49 | func load_subtitles_file() -> Variant: 50 | var p: String = FOLDER_PATH % [id, "subtitles.txt"] 51 | if not FileAccess.file_exists(p): 52 | printerr("No subtitles found in %s" % p) 53 | return [] 54 | 55 | var file := FileAccess.open(p, FileAccess.READ) 56 | var sub := [] 57 | while not file.eof_reached(): 58 | var file_line := file.get_line() 59 | var line := file_line.split("\t") 60 | 61 | if line[0] == "": 62 | continue 63 | 64 | sub.push_back([ 65 | line[0] as float, 66 | line[2] 67 | ]) 68 | 69 | return sub 70 | 71 | func load(type: String) -> AudioStreamOggVorbis: 72 | var p: String = self.path % type 73 | assert(FileAccess.file_exists(p), "No audio file found in %s" % p) 74 | 75 | var file := FileAccess.open(p, FileAccess.READ) 76 | return AudioStreamOggVorbis.load_from_buffer(file.get_buffer(file.get_length())) 77 | 78 | func get_song_ui_name() -> String: 79 | return self.short_name if self.short_name else self.song_name 80 | -------------------------------------------------------------------------------- /scripts/shared/templates.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Templates 3 | 4 | # region STATS 5 | 6 | static var obs_stats_template: String = "" \ 7 | + "Active FPS: [b]{activeFps}[/b]\n" \ 8 | + "CPU Usage: [b]{cpuUsage}%[/b]\n" \ 9 | + "Memory Usage: [b]{memoryUsage} MB[/b]\n" \ 10 | + "Disk Space: [b]{availableDiskSpace} GB[/b]\n" \ 11 | + "Frame Render Time: [b]{averageFrameRenderTime} ms[/b]\n" \ 12 | + "Rendered Skipped: [b]{renderSkippedFrames}[/b]\n" \ 13 | + "Total Skipped: [b]{outputSkippedFrames}[/b]\n" \ 14 | + "WS In/Out: [b]{webSocketSessionIncomingMessages}[/b]/[b]{webSocketSessionOutgoingMessages}[/b]" 15 | 16 | static var godot_stats_template: String = "" \ 17 | + "Active FPS: [b]{fps}[/b]\n" \ 18 | + "Frame Time: [b]{frameTime} s[/b]\n" \ 19 | + "VRAM Used: [b]{videoMemoryUsed} MB[/b]\n" \ 20 | + "Audio Latency: [b]{audioLatency} ms[/b]\n" \ 21 | + "Audio Comp: [b]{audioCompensation} ms[/b]" 22 | 23 | static var backend_stats_template: String = "" \ 24 | + "Messages In/Out: [b]{0}[/b]/[b]{1}[/b]" 25 | 26 | static var message_queue_stats_template: String = "" \ 27 | + "Queue length: [b]%s[/b]" 28 | 29 | static func format_obs_stats(res: Dictionary) -> String: 30 | var stats := { 31 | "activeFps": snapped(res.activeFps, 0), 32 | "cpuUsage": snapped(res.cpuUsage, 0.001), 33 | "memoryUsage": snapped(res.memoryUsage, 0.1), 34 | "availableDiskSpace": snapped(res.availableDiskSpace / 1024, 0.1), 35 | "averageFrameRenderTime": snapped(res.averageFrameRenderTime, 0.1), 36 | "renderTotalFrames": res.renderTotalFrames, 37 | "renderSkippedFrames": res.renderSkippedFrames, 38 | "outputTotalFrames": res.outputTotalFrames, 39 | "outputSkippedFrames": res.outputSkippedFrames, 40 | "webSocketSessionIncomingMessages": res.webSocketSessionIncomingMessages, 41 | "webSocketSessionOutgoingMessages": res.webSocketSessionOutgoingMessages, 42 | } 43 | 44 | return obs_stats_template.format(stats) 45 | 46 | static func format_godot_stats() -> String: 47 | var stats := { 48 | "fps": _perf_mon("TIME_FPS"), 49 | "frameTime": snapped(_perf_mon("TIME_PROCESS"), 0.0001), 50 | "videoMemoryUsed": snapped(_perf_mon("RENDER_VIDEO_MEM_USED") / 1024 / 1000, 0.01), 51 | "audioLatency": snapped(_perf_mon("AUDIO_OUTPUT_LATENCY"), 0.0001), 52 | "audioCompensation": snapped(Globals.get_audio_compensation(), 0.0001) 53 | } 54 | 55 | return godot_stats_template.format(stats) 56 | 57 | static func format_backend_stats(res: Array) -> String: 58 | return backend_stats_template.format(res) 59 | 60 | static func format_message_queue_stats() -> String: 61 | return message_queue_stats_template % SpeechManager.messages.size() 62 | 63 | static func _perf_mon(monitor: String) -> Variant: 64 | return Performance.get_monitor(Performance[monitor]) 65 | 66 | # endregion 67 | 68 | # region NODES 69 | 70 | static var filter_node_name := "Filter_%s_%s" 71 | static var scene_node_name := "Scene_%s" 72 | 73 | # endregion 74 | -------------------------------------------------------------------------------- /scripts/shared/variables.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Variables 3 | 4 | static var default_model_position := Vector2(740, 1160) 5 | static var default_model_scale := 0.33 6 | static var default_lower_third_position := [Vector2(35, 682), Vector2(1, 1)] 7 | 8 | static var positions := { 9 | # use Tulpes - [ position, scale ] 10 | "intro": {}, # placeholder for intro animation 11 | 12 | "under": { 13 | "model": [Vector2(740, 2080), 0.33], 14 | "lower_third": default_lower_third_position, 15 | }, 16 | 17 | "default": { 18 | "model": [default_model_position, default_model_scale], 19 | "lower_third": default_lower_third_position, 20 | }, 21 | 22 | "gaming": { 23 | "model": [Vector2(1680, 1420), 0.3], 24 | "lower_third": [Vector2(35, 800), Vector2(0.777, 0.777)], 25 | }, 26 | 27 | "close": { 28 | "model": [Vector2(740, 1500), 0.5], 29 | "lower_third": default_lower_third_position, 30 | }, 31 | 32 | "fullscreen": { 33 | "model": [Vector2(920, 3265), 1.35], 34 | "lower_third": default_lower_third_position, 35 | }, 36 | 37 | "full_height": { 38 | "model": [Vector2(740, 520), 0.15], 39 | "lower_third": default_lower_third_position, 40 | }, 41 | 42 | "collab": { 43 | "model": [Vector2(362, 963.8469), 0.28], 44 | "lower_third": [Vector2(35, 700), Vector2(0.777, 0.777)], 45 | }, 46 | 47 | "collab_song": { 48 | "model": [Vector2(377, 1054), default_model_scale], 49 | "lower_third": default_lower_third_position, 50 | } 51 | } 52 | 53 | static var pinnable_assets := { 54 | "censor": PinnableAsset.new("Censor", "Nose", Vector2(0, -120), 1.5), 55 | "glasses": PinnableAsset.new("Glasses", "Nose", Vector2(10, -110), 1.1), 56 | "hat": PinnableAsset.new("Hat", "ArtMesh67", Vector2(90, 100), 1.1), 57 | "band": PinnableAsset.new("TetoBand", "ArtMesh30", Vector2(-75, 70), 1.0) 58 | } 59 | 60 | static var toggles := { 61 | "toast": Toggle.new("Param9", 0.5), 62 | "void": Toggle.new("Param14", 0.5), 63 | "tears": Toggle.new("Param20", 0.5), 64 | "toa": Toggle.new("Param21", 1.0), 65 | "confused": Toggle.new("Param18", 0.5), 66 | "gymbag": Toggle.new("Param28", 0.5) 67 | } 68 | 69 | static var animations := { 70 | "idle1": Live2DAnimation.new(0, 7), # Original: 8.067 71 | "idle2": Live2DAnimation.new(1, 4), # Original: 4.267 72 | "idle3": Live2DAnimation.new(2, 5), # Original: 5.367 73 | "sleep": Live2DAnimation.new(3, 10.3, true), # Original: 10.3 74 | "confused": Live2DAnimation.new(4, 4.0, true) # Original: 10 75 | } 76 | 77 | static var emotions_modifiers := { 78 | # Negative 79 | "anger": - 1.0, 80 | "disappointment": - 0.5, 81 | "disgust": - 0.5, 82 | "embarrassment": - 0.3, 83 | "fear": - 0.3, 84 | "grief": - 0.3, 85 | "annoyance": - 0.1, 86 | "confusion": - 0.1, 87 | "sadness": - 0.1, 88 | 89 | # Neutral 90 | "admiration": 0.0, 91 | "approval": 0.0, 92 | "caring": 0.0, 93 | "curiosity": 0.0, 94 | "desire": 0.0, 95 | "disapproval": 0.0, 96 | "gratitude": 0.0, 97 | "nervousness": 0.0, 98 | "pride": 0.0, 99 | "realization": 0.0, 100 | "relief": 0.0, 101 | "remorse": 0.0, 102 | "neutral": 0.0, 103 | "anticipation": 0.0, 104 | 105 | # Positive 106 | "amusement": 0.5, 107 | "excitement": 0.5, 108 | "joy": 0.5, 109 | "love": 0.5, 110 | "surprise": 0.5, 111 | "optimism": 0.1, 112 | } 113 | -------------------------------------------------------------------------------- /scripts/shared/websocket_client.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name WebSocketClient 3 | 4 | const URL_PATH: String = "%s://%s:%s" 5 | var socket: WebSocketPeer 6 | var last_state: WebSocketPeer.State = WebSocketPeer.STATE_CLOSED 7 | 8 | var secure: String = "wss" if Globals.config.get_backend("secure") else "ws" 9 | var host: String = Globals.config.get_backend("host") 10 | var port: String = Globals.config.get_backend("port") 11 | var password: String = Globals.config.get_backend("password") 12 | 13 | var handshake_headers: PackedStringArray = PackedStringArray([ 14 | "Authentication: Bearer %s" % password 15 | ]) 16 | @export var supported_protocols: PackedStringArray 17 | var tls_options: TLSOptions = null 18 | 19 | signal connection_established() 20 | signal connection_closed() 21 | signal data_received(data: Variant, stats: Array) 22 | 23 | # Stats 24 | var incoming_messages_count: int = 0 25 | var outgoing_messages_count: int = 0 26 | 27 | func _ready() -> void: 28 | set_process(false) 29 | 30 | func _process(_delta: float) -> void: 31 | if socket.get_ready_state() != WebSocketPeer.STATE_CLOSED: 32 | socket.poll() 33 | 34 | var state: WebSocketPeer.State = socket.get_ready_state() 35 | if last_state != state: 36 | last_state = state 37 | 38 | match state: 39 | WebSocketPeer.STATE_OPEN: 40 | connection_established.emit() 41 | 42 | WebSocketPeer.STATE_CLOSED: 43 | var code = socket.get_close_code() 44 | var reason = socket.get_close_reason() 45 | printerr("Toaster client: Connection closed with code %d, reason %s. Clean: %s" % [code, reason, code != -1]) 46 | 47 | connection_closed.emit() 48 | set_process(false) 49 | 50 | if state == WebSocketPeer.STATE_OPEN: 51 | while socket.get_available_packet_count(): 52 | incoming_messages_count += 1 53 | data_received.emit(socket.get_packet(), [incoming_messages_count, outgoing_messages_count]) 54 | 55 | func is_open() -> bool: 56 | return socket.get_ready_state() == WebSocketPeer.STATE_OPEN 57 | 58 | func connect_client() -> void: 59 | print("Toaster client: Establishing connection...") 60 | 61 | socket = WebSocketPeer.new() 62 | 63 | socket.handshake_headers = handshake_headers 64 | socket.supported_protocols = supported_protocols 65 | socket.inbound_buffer_size = 200000000 66 | 67 | var err: Error = socket.connect_to_url(URL_PATH % [secure, host, port], TLSOptions.client()) 68 | if err != OK: 69 | printerr("Toaster client: Connection error ", err) 70 | connection_closed.emit() 71 | 72 | last_state = socket.get_ready_state() 73 | print("Toaster client: Connecting...") 74 | set_process(true) 75 | 76 | func send_message(json: Dictionary) -> void: 77 | var message: String = JSON.stringify(json) 78 | 79 | outgoing_messages_count += 1 80 | 81 | if not socket or socket.get_ready_state() != WebSocketPeer.STATE_OPEN: 82 | printerr("Toaster client: Socket connection is not established") 83 | 84 | var err: Error = socket.send_text(message) 85 | if err != OK: 86 | printerr("Toaster client: Message sending error ", err) 87 | 88 | func break_connection(reason: String = "") -> void: 89 | socket.close(1000, reason) 90 | last_state = socket.get_ready_state() 91 | 92 | incoming_messages_count = 0 93 | outgoing_messages_count = 0 94 | -------------------------------------------------------------------------------- /toast.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOM-Network/Melba-Toaster/87c9f5b1e1aa6a0241674f8e75197e0d65c5d28f/toast.ico --------------------------------------------------------------------------------