Compare commits

...

19 commits

Author SHA1 Message Date
199f2cdc9d Updated ScriptEditor
Ensure that when we fetch the source code from the editor, we replace
any tab characters with 4 spaces.
2026-03-02 02:18:06 -06:00
ba537bc791 Updated GeneralPanel
Updated UI to have Section for Display
Added logic to handle Authorizing Streamer and Bot.
Added logic to handle Connecting to OBS.
Added Loading/Saving Logic for other settings in the General Panel.
2026-03-02 02:17:42 -06:00
73615a9bb4 Alerts Update
Implemented base class Alert.
Implemented Alerts for Cheering Bits, Following, Raid Alert, and
Subscription alerts.
Added Fonts and Video media.
2026-03-02 02:15:30 -06:00
5f8df9bf7b Updated FloatingMenu
Added Testing parameters for Alerts, so we can test alerts being used.
Added Camera variable @onready fetch.
Added OBS and VTuber Process ID, Camera and VTuber Scene ID.
Connected signal to get Scene ID's when we connect to OBS.
Added event handlers for Step Array (Testing Alerts), VTuber launcher,
and Camera toggle to switch between VTuber and Camera.
2026-03-02 02:14:07 -06:00
4e808314d2 Updated SettingsPanel
Added Music tab for Music playing.
Ensure Chat Avatars tab isn't the first tab that we show when we open.
2026-03-02 02:11:26 -06:00
689dcdc092 Updated TwitcherExtended
Added bool fields for streamer and chatbot token loaded, so that it can
be checked in the overlay.
2026-03-02 02:10:41 -06:00
948fb00405 Updated project
Added ObsManager to Autoload.
2026-03-02 02:09:36 -06:00
09dd6cb2e2 Updated MainWin
Added ChatBox to the window.
Updated MainWin to handle setting Globals.main_win.
Added code to handle ensuring the overlay is on the correct montior
chosen by user.
Added check for connecting to twitch automatically.
Added Subscriptions, to setup subscriptions upon connection.
Added ObsManager connection to scene changed.
Added ObsManager connection on startup.
Added handling of scene changed, adjusting ChatBox to dock to a specific
location.
2026-03-02 02:09:21 -06:00
feba26839b Created ChatBox UI
Created ChatBox UI for display chat in the Overlay.
2026-03-02 02:07:05 -06:00
54901a14b9 Updated Globals
Added main_win to the Globals.
2026-03-02 02:06:17 -06:00
9206793ebc Updated EventManager
Added queue for alerts, so that they aren't all firing off in one time.
Updated ready to check if the root has the MainWin node (When running a
specific scene, may not be present)
Added function to queue up alert, and processing of queued alerts.
2026-03-02 02:06:01 -06:00
d44f1dbfb5 Created ObsManager class
Created manager to handle OBS Interaction with OBS WebSocket.
2026-03-02 02:04:03 -06:00
e7a1732b0f Updated OverlaySettings
Added OBS Path, OBS Process name, OBS Reconnect, VTuber Path, VTuber
Process name, VTuber Model path, Display screen options to our Overlay
settings.
2026-03-02 02:03:39 -06:00
6cccc36ecd Created class ProcessTree
ProcessTree creates a way to see what processes are running on the
computer.  This is used in running OBS/VeadoTube.
2026-03-02 02:02:59 -06:00
b52f62cc59 Updated Audio Bus
Lowered the default volume for the master bus, since it was louder then
needed.  Will make this adjustable in the overlay.
2026-03-02 02:02:04 -06:00
b033741431 Created Tests to workout ProcessTree
Test work to see what is needed to create ProcessTree, should work on
*nix based systems, such as MacOS and Linux, since ps is available
there.  Need to work on windows with tasklist.
2026-03-02 02:01:30 -06:00
e02daac0ff Created Plugin Custom Runner
Created plugin Custom Runner, but is not specifically needed for what it
was originally created for.  Still checking it in, since it might be
useful later.
2026-03-02 02:00:30 -06:00
3804940b22 Updated no-obs-ws
Added check to see if _auth_required is already connected to.
2026-03-02 01:59:52 -06:00
e4637479e9 Updated GDE GoZen
Updated GDE GoZen to 9.2 (Custom Linux build for Transparent VP9 Video)
2026-03-02 01:59:33 -06:00
74 changed files with 2296 additions and 69 deletions

5
UI/Alerts/alert.gd Normal file
View file

@ -0,0 +1,5 @@
extends Control
class_name Alert
func dispose() -> void:
queue_free()

1
UI/Alerts/alert.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bw0rk6o7xj67e

26
UI/Alerts/cheer_alert.gd Normal file
View file

@ -0,0 +1,26 @@
@tool
extends Alert
class_name CheerAlert
@onready var sub_text: Label = %SubText
@onready var main_text: Label = %MainText
@onready var message_text: Label = %MessageText
@onready var timeline: AnimationPlayer = %Timeline
var _chatter: String = "None"
var _msg: String = ""
var _bits: int = 0
func setup(chatter: String, bits: int, message: String) -> void:
_chatter = chatter
_bits = bits
_msg = message
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
main_text.text = _chatter
sub_text.text = "x%d bits" % _bits
message_text.text = _msg
if Engine.is_editor_hint(): return
timeline.play(&"default")

View file

@ -0,0 +1 @@
uid://crl8eyb3nahln

278
UI/Alerts/cheer_alert.tscn Normal file
View file

@ -0,0 +1,278 @@
[gd_scene format=3 uid="uid://d05mpml5k78u6"]
[ext_resource type="Script" uid="uid://ivx7m2bfysip" path="res://addons/gde_gozen/video_playback.gd" id="1_7rajh"]
[ext_resource type="Script" uid="uid://crl8eyb3nahln" path="res://UI/Alerts/cheer_alert.gd" id="1_eam5w"]
[ext_resource type="FontFile" uid="uid://cx1a4aqqxhsrn" path="res://UI/assets/neon-wave-theme/polentical_neon/Polentical Neon Italic.ttf" id="2_eam5w"]
[ext_resource type="FontFile" uid="uid://c30qqiv6sqheh" path="res://UI/assets/fonts/rage.woff2" id="3_vhkt3"]
[sub_resource type="LabelSettings" id="LabelSettings_t8pat"]
font = ExtResource("3_vhkt3")
font_size = 48
font_color = Color(0, 1, 1, 1)
stacked_shadow_count = 2
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.35822138, 0.0011343455, 0.8658768, 1)
stacked_shadow_0/outline_size = 3
stacked_shadow_1/offset = Vector2(4, 4)
stacked_shadow_1/color = Color(1, 0.5023, 0.43550968, 1)
stacked_shadow_1/outline_size = 5
[sub_resource type="LabelSettings" id="LabelSettings_jsg8g"]
font = ExtResource("2_eam5w")
font_size = 60
font_color = Color(0.99999994, 0.6120906, 0.16155985, 1)
stacked_shadow_count = 4
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.5568628, 0.003921569, 1, 1)
stacked_shadow_0/outline_size = 2
stacked_shadow_1/offset = Vector2(3, 3)
stacked_shadow_1/color = Color(0.9411765, 0.003921569, 1, 1)
stacked_shadow_1/outline_size = 2
stacked_shadow_2/offset = Vector2(4, 4)
stacked_shadow_2/color = Color(0.9843137, 0.25882354, 0.7254902, 1)
stacked_shadow_2/outline_size = 2
stacked_shadow_3/offset = Vector2(5, 5)
stacked_shadow_3/color = Color(0.99215686, 0.46666667, 0.8039216, 1)
stacked_shadow_3/outline_size = 2
[sub_resource type="LabelSettings" id="LabelSettings_vhkt3"]
font_size = 24
[sub_resource type="Animation" id="Animation_jsg8g"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("SubText:modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MainText:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MessageText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
[sub_resource type="Animation" id="Animation_t8pat"]
resource_name = "default"
length = 10.0
tracks/0/type = "method"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Background")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/1/type = "method"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Icon")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("SubText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 3, 4, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MainText:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 2, 3, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/4/type = "method"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath(".")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(10),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"dispose"
}]
}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("MessageText:modulate")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {
"times": PackedFloat32Array(0, 4, 5, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_jsg8g"]
_data = {
&"RESET": SubResource("Animation_jsg8g"),
&"default": SubResource("Animation_t8pat")
}
[node name="CheerAlert" type="Control" unique_id=95763662]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_eam5w")
[node name="Background" type="Control" parent="." unique_id=939332259]
unique_name_in_owner = true
custom_minimum_size = Vector2(960, 540)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -480.0
offset_top = -270.0
offset_right = 480.0
offset_bottom = 270.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_7rajh")
path = "res://UI/assets/video/Backgrounds/Lightning-Synthwave.webm"
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="Icon" type="Control" parent="." unique_id=823113899]
unique_name_in_owner = true
custom_minimum_size = Vector2(470, 260)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -195.0
offset_top = -303.0
offset_right = 275.0
offset_bottom = -43.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_7rajh")
path = "res://UI/assets/video/Backgrounds/Bits-Synthwave.webm"
enable_audio = false
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="SubText" type="Label" parent="." unique_id=1794185310]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -183.0
offset_top = -85.0
offset_right = 183.0
offset_bottom = -25.0
grow_horizontal = 2
grow_vertical = 2
text = "x40 Bits"
label_settings = SubResource("LabelSettings_t8pat")
horizontal_alignment = 1
[node name="MainText" type="Label" parent="." unique_id=757351879]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -145.0
offset_top = -37.5
offset_right = 145.0
offset_bottom = 37.5
grow_horizontal = 2
grow_vertical = 2
text = "None"
label_settings = SubResource("LabelSettings_jsg8g")
horizontal_alignment = 1
[node name="MessageText" type="Label" parent="." unique_id=79076647]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
custom_minimum_size = Vector2(1000, 200)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -500.0
offset_top = 55.0
offset_right = 500.0
offset_bottom = 255.0
grow_horizontal = 2
grow_vertical = 2
text = "Born to late to explore Earth, born to early to explore the universe"
label_settings = SubResource("LabelSettings_vhkt3")
horizontal_alignment = 1
autowrap_mode = 2
max_lines_visible = 5
[node name="Timeline" type="AnimationPlayer" parent="." unique_id=1438475424]
unique_name_in_owner = true
libraries/ = SubResource("AnimationLibrary_jsg8g")

16
UI/Alerts/follow_alert.gd Normal file
View file

@ -0,0 +1,16 @@
@tool
extends Alert
class_name FollowAlert
@onready var main_text: Label = %MainText
@onready var timeline: AnimationPlayer = %Timeline
var _chatter: String = "None"
func setup(chatter: String) -> void:
_chatter = chatter
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
main_text.text = _chatter
if Engine.is_editor_hint(): return
timeline.play(&"default")

View file

@ -0,0 +1 @@
uid://cdfp46glq557p

226
UI/Alerts/follow_alert.tscn Normal file
View file

@ -0,0 +1,226 @@
[gd_scene format=3 uid="uid://dr2di6iv0mdkh"]
[ext_resource type="Script" uid="uid://cdfp46glq557p" path="res://UI/Alerts/follow_alert.gd" id="1_44jhq"]
[ext_resource type="Script" uid="uid://ivx7m2bfysip" path="res://addons/gde_gozen/video_playback.gd" id="1_86a2m"]
[ext_resource type="FontFile" uid="uid://cx1a4aqqxhsrn" path="res://UI/assets/neon-wave-theme/polentical_neon/Polentical Neon Italic.ttf" id="3_t8pat"]
[sub_resource type="LabelSettings" id="LabelSettings_t8pat"]
font_size = 48
font_color = Color(0, 1, 1, 1)
stacked_shadow_count = 2
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.35822138, 0.0011343455, 0.8658768, 1)
stacked_shadow_0/outline_size = 3
stacked_shadow_1/offset = Vector2(4, 4)
stacked_shadow_1/color = Color(1, 0.5023, 0.43550968, 1)
stacked_shadow_1/outline_size = 5
[sub_resource type="LabelSettings" id="LabelSettings_jsg8g"]
font = ExtResource("3_t8pat")
font_size = 60
font_color = Color(0.99999994, 0.6120906, 0.16155985, 1)
stacked_shadow_count = 4
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.5568628, 0.003921569, 1, 1)
stacked_shadow_0/outline_size = 2
stacked_shadow_1/offset = Vector2(3, 3)
stacked_shadow_1/color = Color(0.9411765, 0.003921569, 1, 1)
stacked_shadow_1/outline_size = 2
stacked_shadow_2/offset = Vector2(4, 4)
stacked_shadow_2/color = Color(0.9843137, 0.25882354, 0.7254902, 1)
stacked_shadow_2/outline_size = 2
stacked_shadow_3/offset = Vector2(5, 5)
stacked_shadow_3/color = Color(0.99215686, 0.46666667, 0.8039216, 1)
stacked_shadow_3/outline_size = 2
[sub_resource type="Animation" id="Animation_jsg8g"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("SubText:modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MainText:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
[sub_resource type="Animation" id="Animation_t8pat"]
resource_name = "default"
length = 10.0
tracks/0/type = "method"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Background")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/1/type = "method"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Icon")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("SubText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 5, 6, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MainText:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 3, 4, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/4/type = "method"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath(".")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(10),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"dispose"
}]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_jsg8g"]
_data = {
&"RESET": SubResource("Animation_jsg8g"),
&"default": SubResource("Animation_t8pat")
}
[node name="FollowAlert" type="Control" unique_id=95763662]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_44jhq")
[node name="Background" type="Control" parent="." unique_id=939332259]
unique_name_in_owner = true
custom_minimum_size = Vector2(960, 540)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -480.0
offset_top = -270.0
offset_right = 480.0
offset_bottom = 270.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_86a2m")
path = "res://UI/assets/video/Backgrounds/Stripes-Synthwave.webm"
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="Icon" type="Control" parent="." unique_id=823113899]
unique_name_in_owner = true
custom_minimum_size = Vector2(470, 260)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -195.0
offset_top = -303.0
offset_right = 275.0
offset_bottom = -43.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_86a2m")
path = "res://UI/assets/video/Icons/Joystick-Synthwave.webm"
enable_audio = false
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="SubText" type="Label" parent="." unique_id=1794185310]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -183.0
offset_top = -85.0
offset_right = 183.0
offset_bottom = -25.0
grow_horizontal = 2
grow_vertical = 2
text = "New Follower"
label_settings = SubResource("LabelSettings_t8pat")
[node name="MainText" type="Label" parent="." unique_id=757351879]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -145.0
offset_top = -37.5
offset_right = 145.0
offset_bottom = 37.5
grow_horizontal = 2
grow_vertical = 2
text = "None"
label_settings = SubResource("LabelSettings_jsg8g")
horizontal_alignment = 1
[node name="Timeline" type="AnimationPlayer" parent="." unique_id=1438475424]
unique_name_in_owner = true
libraries/ = SubResource("AnimationLibrary_jsg8g")

22
UI/Alerts/raid_alert.gd Normal file
View file

@ -0,0 +1,22 @@
@tool
extends Alert
class_name RaidAlert
@onready var sub_text: Label = %SubText
@onready var main_text: Label = %MainText
@onready var timeline: AnimationPlayer = %Timeline
var _chatter: String = "None"
var _raiders: int = 0
func setup(chatter: String, raiders: int) -> void:
_chatter = chatter
_raiders = raiders
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
main_text.text = _chatter
sub_text.text = "Raid x%d" % _raiders
if Engine.is_editor_hint(): return
timeline.play(&"default")

View file

@ -0,0 +1 @@
uid://cns0ihwkp2i1c

229
UI/Alerts/raid_alert.tscn Normal file
View file

@ -0,0 +1,229 @@
[gd_scene format=3 uid="uid://b6jis61uxnhf3"]
[ext_resource type="Script" uid="uid://cns0ihwkp2i1c" path="res://UI/Alerts/raid_alert.gd" id="1_ur8rv"]
[ext_resource type="Script" uid="uid://ivx7m2bfysip" path="res://addons/gde_gozen/video_playback.gd" id="1_yqbii"]
[ext_resource type="FontFile" uid="uid://cx1a4aqqxhsrn" path="res://UI/assets/neon-wave-theme/polentical_neon/Polentical Neon Italic.ttf" id="2_ur8rv"]
[ext_resource type="FontFile" uid="uid://c30qqiv6sqheh" path="res://UI/assets/fonts/rage.woff2" id="3_6v7ty"]
[sub_resource type="LabelSettings" id="LabelSettings_t8pat"]
font = ExtResource("3_6v7ty")
font_size = 64
font_color = Color(0.60239375, 0.9999997, 0.3363843, 1)
stacked_shadow_count = 2
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.35822138, 0.0011343455, 0.8658768, 1)
stacked_shadow_0/outline_size = 3
stacked_shadow_1/offset = Vector2(4, 4)
stacked_shadow_1/color = Color(1, 0.5023, 0.43550968, 1)
stacked_shadow_1/outline_size = 5
[sub_resource type="LabelSettings" id="LabelSettings_jsg8g"]
font = ExtResource("2_ur8rv")
font_size = 60
font_color = Color(0.99999994, 0.6120906, 0.16155985, 1)
stacked_shadow_count = 4
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.5568628, 0.003921569, 1, 1)
stacked_shadow_0/outline_size = 2
stacked_shadow_1/offset = Vector2(3, 3)
stacked_shadow_1/color = Color(0.9411765, 0.003921569, 1, 1)
stacked_shadow_1/outline_size = 2
stacked_shadow_2/offset = Vector2(4, 4)
stacked_shadow_2/color = Color(0.9843137, 0.25882354, 0.7254902, 1)
stacked_shadow_2/outline_size = 2
stacked_shadow_3/offset = Vector2(5, 5)
stacked_shadow_3/color = Color(0.99215686, 0.46666667, 0.8039216, 1)
stacked_shadow_3/outline_size = 2
[sub_resource type="Animation" id="Animation_jsg8g"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("SubText:modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MainText:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
[sub_resource type="Animation" id="Animation_t8pat"]
resource_name = "default"
length = 10.0
tracks/0/type = "method"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Background")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/1/type = "method"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Icon")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("SubText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 5, 6, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MainText:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 3, 4, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/4/type = "method"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath(".")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(10),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"dispose"
}]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_jsg8g"]
_data = {
&"RESET": SubResource("Animation_jsg8g"),
&"default": SubResource("Animation_t8pat")
}
[node name="RaidAlert" type="Control" unique_id=95763662]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_ur8rv")
[node name="Background" type="Control" parent="." unique_id=939332259]
unique_name_in_owner = true
custom_minimum_size = Vector2(960, 540)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -480.0
offset_top = -270.0
offset_right = 480.0
offset_bottom = 270.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_yqbii")
path = "res://UI/assets/video/Backgrounds/Grid-Synthwave.webm"
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="Icon" type="Control" parent="." unique_id=823113899]
unique_name_in_owner = true
custom_minimum_size = Vector2(470, 260)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -195.0
offset_top = -303.0
offset_right = 275.0
offset_bottom = -43.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_yqbii")
path = "res://UI/assets/video/Icons/Computer-Synthwave.webm"
enable_audio = false
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="SubText" type="Label" parent="." unique_id=1794185310]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -183.0
offset_top = -110.0
offset_right = 183.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
text = "Raid x10"
label_settings = SubResource("LabelSettings_t8pat")
horizontal_alignment = 1
[node name="MainText" type="Label" parent="." unique_id=757351879]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -145.0
offset_top = -37.5
offset_right = 145.0
offset_bottom = 37.5
grow_horizontal = 2
grow_vertical = 2
text = "None"
label_settings = SubResource("LabelSettings_jsg8g")
horizontal_alignment = 1
[node name="Timeline" type="AnimationPlayer" parent="." unique_id=1438475424]
unique_name_in_owner = true
libraries/ = SubResource("AnimationLibrary_jsg8g")

View file

@ -0,0 +1,39 @@
@tool
extends Alert
class_name SubscriberAlert
@onready var sub_text: Label = %SubText
@onready var main_text: Label = %MainText
@onready var message_text: Label = %MessageText
@onready var timeline: AnimationPlayer = %Timeline
var _chatter: String = "None"
var _msg: String = ""
var _months: int = 0
var _prime: bool = false
var _tier: int = 1
func setup(chatter: String, months: int, prime: bool, tier: int, message: String) -> void:
_chatter = chatter
_months = months
_msg = message
_prime = prime
_tier = tier
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
main_text.text = _chatter
if _months <= 1:
if _prime:
sub_text.text = "New Prime Sub"
else:
sub_text.text = "New Teir %d Sub" % _tier
else:
if _prime:
sub_text.text = "Prime Resub x%d" % _months
else:
sub_text.text = "Teir %d Resub x%d" % [_tier, _months]
message_text.text = _msg
if Engine.is_editor_hint(): return
timeline.play(&"default")

View file

@ -0,0 +1 @@
uid://c6byqfgqyxfbh

View file

@ -0,0 +1,278 @@
[gd_scene format=3 uid="uid://b3o8lxueafbqq"]
[ext_resource type="Script" uid="uid://ivx7m2bfysip" path="res://addons/gde_gozen/video_playback.gd" id="1_b3tp1"]
[ext_resource type="Script" uid="uid://c6byqfgqyxfbh" path="res://UI/Alerts/subscriber_alert.gd" id="1_f1hvy"]
[ext_resource type="FontFile" uid="uid://cx1a4aqqxhsrn" path="res://UI/assets/neon-wave-theme/polentical_neon/Polentical Neon Italic.ttf" id="2_38hii"]
[ext_resource type="FontFile" uid="uid://o4e6chcu01yx" path="res://UI/assets/fonts/Yellowtail.woff2" id="3_4l3sh"]
[sub_resource type="LabelSettings" id="LabelSettings_t8pat"]
font = ExtResource("3_4l3sh")
font_size = 64
font_color = Color(0, 1, 1, 1)
stacked_shadow_count = 2
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.35822138, 0.0011343455, 0.8658768, 1)
stacked_shadow_0/outline_size = 3
stacked_shadow_1/offset = Vector2(4, 4)
stacked_shadow_1/color = Color(1, 0.5023, 0.43550968, 1)
stacked_shadow_1/outline_size = 5
[sub_resource type="LabelSettings" id="LabelSettings_jsg8g"]
font = ExtResource("2_38hii")
font_size = 60
font_color = Color(0.99999994, 0.6120906, 0.16155985, 1)
stacked_shadow_count = 4
stacked_shadow_0/offset = Vector2(2, 2)
stacked_shadow_0/color = Color(0.5568628, 0.003921569, 1, 1)
stacked_shadow_0/outline_size = 2
stacked_shadow_1/offset = Vector2(3, 3)
stacked_shadow_1/color = Color(0.9411765, 0.003921569, 1, 1)
stacked_shadow_1/outline_size = 2
stacked_shadow_2/offset = Vector2(4, 4)
stacked_shadow_2/color = Color(0.9843137, 0.25882354, 0.7254902, 1)
stacked_shadow_2/outline_size = 2
stacked_shadow_3/offset = Vector2(5, 5)
stacked_shadow_3/color = Color(0.99215686, 0.46666667, 0.8039216, 1)
stacked_shadow_3/outline_size = 2
[sub_resource type="LabelSettings" id="LabelSettings_dx50j"]
font_size = 24
[sub_resource type="Animation" id="Animation_jsg8g"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("SubText:modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MainText:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MessageText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
[sub_resource type="Animation" id="Animation_t8pat"]
resource_name = "default"
length = 10.0
tracks/0/type = "method"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Background")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/1/type = "method"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Icon")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"play"
}]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("SubText:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 3, 4, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MainText:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 2, 3, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/4/type = "method"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath(".")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(10),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"dispose"
}]
}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("MessageText:modulate")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {
"times": PackedFloat32Array(0, 4, 5, 9, 10),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_f1hvy"]
_data = {
&"RESET": SubResource("Animation_jsg8g"),
&"default": SubResource("Animation_t8pat")
}
[node name="SubscriberAlert" type="Control" unique_id=95763662]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_f1hvy")
[node name="Background" type="Control" parent="." unique_id=939332259]
unique_name_in_owner = true
custom_minimum_size = Vector2(960, 540)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -480.0
offset_top = -270.0
offset_right = 480.0
offset_bottom = 270.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_b3tp1")
path = "res://UI/assets/video/Backgrounds/Triangle-Synthwave.webm"
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="Icon" type="Control" parent="." unique_id=823113899]
unique_name_in_owner = true
custom_minimum_size = Vector2(470, 260)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -234.0
offset_top = -303.0
offset_right = 236.0
offset_bottom = -43.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_b3tp1")
path = "res://UI/assets/video/Icons/Hoverboard-Synthwave.webm"
enable_audio = false
metadata/_custom_type_script = "uid://ivx7m2bfysip"
[node name="SubText" type="Label" parent="." unique_id=1794185310]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -183.0
offset_top = -85.0
offset_right = 183.0
offset_bottom = -25.0
grow_horizontal = 2
grow_vertical = 2
text = "New Sub"
label_settings = SubResource("LabelSettings_t8pat")
horizontal_alignment = 1
[node name="MainText" type="Label" parent="." unique_id=757351879]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -145.0
offset_top = -37.5
offset_right = 145.0
offset_bottom = 37.5
grow_horizontal = 2
grow_vertical = 2
text = "None"
label_settings = SubResource("LabelSettings_jsg8g")
horizontal_alignment = 1
[node name="MessageText" type="Label" parent="." unique_id=4513558]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0)
custom_minimum_size = Vector2(1000, 200)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -500.0
offset_top = 55.0
offset_right = 500.0
offset_bottom = 255.0
grow_horizontal = 2
grow_vertical = 2
text = "Born to late to explore Earth, born to early to explore the universe"
label_settings = SubResource("LabelSettings_dx50j")
horizontal_alignment = 1
autowrap_mode = 2
max_lines_visible = 5
[node name="Timeline" type="AnimationPlayer" parent="." unique_id=512554623]
unique_name_in_owner = true
libraries/ = SubResource("AnimationLibrary_f1hvy")

139
UI/Controls/chat_box.gd Normal file
View file

@ -0,0 +1,139 @@
@tool
extends Control
class_name ChatBox
enum ChatDock { BOTTOM, LEFT, TOP, RIGHT }
@onready var chat_history: ScrollContainer = %ChatHistory
@onready var history: VBoxContainer = %History
@onready var height: int = 250
@onready var width: int = 200
@export var dock: ChatDock = ChatDock.BOTTOM:
set(value):
dock = value
if not is_node_ready(): return
_orientate_chatbox()
func _ready() -> void:
var waiting_for_ready := false
if Engine.is_editor_hint():
return
while Globals.twitcher == null or !Globals.twitcher.is_node_ready():
if not waiting_for_ready:
print("Waiting for Twitcher Ready...")
waiting_for_ready = true
await get_tree().process_frame
print("Twitcher ready!")
_orientate_chatbox()
Globals.twitcher.chat.message_received.connect(_handle_chat)
func _orientate_chatbox() -> void:
match dock:
ChatDock.BOTTOM:
chat_history.custom_minimum_size = Vector2(1550,265)
chat_history.size = Vector2(1550,265)
chat_history.position = Vector2(0, get_viewport_rect().size.y - 265)
#chat_history.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
#chat_history.custom_minimum_size = Vector2(0,height)
ChatDock.LEFT:
chat_history.custom_minimum_size = Vector2(500,get_viewport_rect().size.y)
chat_history.size = Vector2(500,get_viewport_rect().size.y)
chat_history.position = Vector2.ZERO
#chat_history.set_anchors_preset(Control.PRESET_LEFT_WIDE)
#chat_history.custom_minimum_size = Vector2(width,0)
ChatDock.RIGHT:
chat_history.custom_minimum_size = Vector2(500, get_viewport_rect().size.y - 265)
chat_history.size = Vector2(500, 265)
chat_history.position = Vector2(get_viewport_rect().size.x - 500, 0)
#chat_history.set_anchors_preset(Control.PRESET_RIGHT_WIDE)
#chat_history.custom_minimum_size = Vector2(width,0)
ChatDock.TOP:
chat_history.custom_minimum_size = Vector2(1550,265)
chat_history.size = Vector2(1550,265)
chat_history.position = Vector2.ZERO
#chat_history.set_anchors_preset(Control.PRESET_TOP_WIDE)
#chat_history.custom_minimum_size = Vector2(0,height)
func _handle_chat(message: TwitchChatMessage) -> void:
var badges_dict: Dictionary = await message.get_badges(Globals.twitcher.media)
var badges: Array[SpriteFrames] = []
badges.assign(badges_dict.values())
var result_message: String = ""
var badge_id: int = 0
for badge: SpriteFrames in badges:
result_message += "[sprite id='b-%s']%s[/sprite]" % [badge_id, badge.resource_path]
badge_id += 1
result_message += "[color=%s]%s[/color]" % [message.get_color(), message.chatter_user_name]
match message.message_type:
TwitchChatMessage.MessageType.text:
result_message = await show_text(message, result_message)
TwitchChatMessage.MessageType.power_ups_gigantified_emote:
result_message = await show_text(message, result_message, 3)
TwitchChatMessage.MessageType.channel_points_highlighted:
result_message += "[bgcolor=#755ebc][color=#e9fffb]"
result_message = await show_text(message, result_message)
result_message += "[/color][/bgcolor]"
TwitchChatMessage.MessageType.power_ups_message_effect:
result_message += "[shake rate=20.0 level=5 connected=1]"
result_message = await show_text(message, result_message)
result_message += "[/shake]"
append_message(result_message)
func show_text(message: TwitchChatMessage, current_text: String, emote_scale: int = 1) -> String:
await message.load_emotes_from_fragment(Globals.twitcher.media)
var frag_id: int = 0
for fragment: TwitchChatMessage.Fragment in message.message.fragments:
frag_id += 1
match fragment.type:
TwitchChatMessage.FragmentType.text:
current_text += fragment.text
TwitchChatMessage.FragmentType.cheermote:
var cheermote_scale: StringName = TwitchCheermoteDefinition.SCALE_MAP.get(emote_scale, TwitchCheermoteDefinition.SCALE_1)
var cheermote: SpriteFrames = await fragment.cheermote.get_sprite_frames(Globals.twitcher.media, cheermote_scale)
current_text += "[sprite id='f-%s']%s[/sprite]" % [frag_id, cheermote.resource_path]
TwitchChatMessage.FragmentType.emote:
var emote: SpriteFrames = await fragment.emote.get_sprite_frames(Globals.twitcher.media, emote_scale)
current_text += "[sprite id='f-%s']%s[/sprite]" % [frag_id, emote.resource_path]
TwitchChatMessage.FragmentType.mention:
current_text += "[color=%s]@%s[/color]" % ["%00a0b6", fragment.mention.user_name]
return current_text
func append_message(msg: String) -> void:
var cm: RichTextLabel = RichTextLabel.new()
cm.bbcode_enabled = true
cm.fit_content = true
var se: SpriteFrameEffect = SpriteFrameEffect.new()
cm.install_effect(se)
%History.add_child(cm)
msg = se.prepare_message(msg, cm)
cm.text = _get_time() + " " + msg
_clean_old_messages()
await get_tree().process_frame
var rect = cm.get_rect()
%ChatHistory.set_deferred("scroll_vertical",rect.position.y + rect.size.y)
func _clean_old_messages() -> void:
var child_count := %ChatHistory.get_child_count()
if child_count < 1000: return
for i in child_count - 1000:
%ChatHistory.get_child(i).queue_free()
func _get_time() -> String:
var time: Dictionary = Time.get_time_dict_from_system()
var pm := false
if time["hour"] >= 12:
pm = true
if time["hour"] > 12:
time["hour"] -= 12
return "%02d:%02d%s" % [time["hour"], time["minute"], "PM" if pm else "AM"]

View file

@ -0,0 +1 @@
uid://dcikt7pf001m8

26
UI/Controls/chat_box.tscn Normal file
View file

@ -0,0 +1,26 @@
[gd_scene format=3 uid="uid://cstq30mjx0pch"]
[ext_resource type="Script" uid="uid://dcikt7pf001m8" path="res://UI/Controls/chat_box.gd" id="1_dg5g0"]
[node name="ChatBox" type="Control" unique_id=859214320]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_dg5g0")
[node name="ChatHistory" type="ScrollContainer" parent="." unique_id=1482465110]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 300)
layout_mode = 0
offset_bottom = 300.0
follow_focus = true
horizontal_scroll_mode = 0
vertical_scroll_mode = 3
[node name="History" type="VBoxContainer" parent="ChatHistory" unique_id=345296200]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3

View file

@ -11,6 +11,15 @@ func _ready() -> void:
%ObsPass.right = EYE_SLASH
%ObsPass.secret = true
%ObsPass.right_button_pressed.connect(_handle_password_show)
%DisplayScreen.item_selected.connect(func(x): Globals.main_win.get_window().current_screen = x)
%UseScreen.pressed.connect(func():
Globals.main_win.get_window().current_screen = ProjectSettings.get_setting("display/window/size/initial_screen")
%DisplayScreen.select(Globals.main_win.get_window().current_screen)
%DisplayScreen.disabled = !%UseScreen.button_pressed
)
%AuthStreamer.pressed.connect(func(): %AuthStreamer.disabled = await Globals.twitcher.authorize_streamer())
%AuthBot.pressed.connect(func(): %AuthBot.disabled = await Globals.twitcher.authorize_chatbot())
%ConnectObs.pressed.connect(func(): ObsManager.connect_to_host())
_load_settings()
func _load_settings() -> void:
@ -19,6 +28,42 @@ func _load_settings() -> void:
%ObsPass.text = Globals.settings.obs_pass
%AutoTwitch.button_pressed = Globals.settings.auto_connect_twitch
%AutoObs.button_pressed = Globals.settings.auto_connect_obs
%ObsRecon.button_pressed = Globals.settings.obs_reconnect
%ConnectTwitch.disabled = Globals.twitcher.eventsub.is_open
%ConnectTwitch.text = "Connect Twitch" if not Globals.twitcher.eventsub.is_open else "Disconnect Twitch"
for scr in DisplayServer.get_screen_count():
%DisplayScreen.add_item("Monitor %d" % (scr + 1))
if Globals.settings.display_screen == -1:
%UseScreen.button_pressed = false
%DisplayScreen.disabled = true
else:
%UseScreen.button_pressed = true
%DisplayScreen.select(Globals.settings.display_screen)
%DisplayScreen.disabled = false
if not Globals.twitcher.streamer_token_loaded:
var res := await Globals.twitcher.load_streamer_token()
if res == TwitcherExtended.AuthStatus.UNAUTHORIZED:
%AuthStreamer.disabled = false
else:
%AuthStreamer.disabled = true
%StreamerName.text = Globals.twitcher.streamer_user.display_name
else:
%AuthStreamer.disabled = true
%StreamerName.text = Globals.twitcher.streamer_user.display_name
if not Globals.twitcher.chatbot_token_loaded:
var res := await Globals.twitcher.load_chatbot_token()
if res == TwitcherExtended.AuthStatus.UNAUTHORIZED:
%AuthBot.disabled = false
else:
%AuthBot.disabled = true
%BotName.text = Globals.twitcher.bot_user.display_name
else:
%AuthBot.disabled = true
%BotName.text = Globals.twitcher.bot_user.display_name
func apply_settings() -> void:
Globals.settings.obs_host = %ObsHost.text
@ -26,6 +71,12 @@ func apply_settings() -> void:
Globals.settings.obs_pass = %ObsPass.text
Globals.settings.auto_connect_twitch = %AutoTwitch.button_pressed
Globals.settings.auto_connect_obs = %AutoObs.button_pressed
Globals.settings.obs_reconnect = %ObsRecon.button_pressed
if %UseScreen.button_pressed:
Globals.settings.display_screen = %DisplayScreen.selected
else:
Globals.settings.display_screen = -1
Globals.main_win.get_window().current_screen = ProjectSettings.get_setting("display/window/size/initial_screen")
func _handle_password_show() -> void:
if %ObsPass.right == EYE_SLASH:
@ -34,3 +85,6 @@ func _handle_password_show() -> void:
else:
%ObsPass.right = EYE_SLASH
%ObsPass.secret = true
func _exit_tree() -> void:
apply_settings()

View file

@ -8,7 +8,7 @@
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_7naex"]
content_margin_left = 10.0
content_margin_top = 8.0
content_margin_right = 78.538
content_margin_right = 181.345
texture = ExtResource("2_7naex")
texture_margin_left = 3.08429
texture_margin_top = 5.50548
@ -19,7 +19,7 @@ region_rect = Rect2(93, 474, 41, 33)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_en626"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 72.538
content_margin_right = 175.345
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.3)
border_width_bottom = 2
@ -33,7 +33,7 @@ corner_detail = 5
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_v8ga3"]
content_margin_left = 10.0
content_margin_top = 8.0
content_margin_right = 78.538
content_margin_right = 181.345
texture = ExtResource("2_7naex")
texture_margin_left = 3.08429
texture_margin_top = 5.50548
@ -218,3 +218,26 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(240, 0)
layout_mode = 2
text = "Connect OBS"
[node name="Control3" type="Control" parent="MarginContainer/VBoxContainer" unique_id=292441254]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="Label4" type="Label" parent="MarginContainer/VBoxContainer" unique_id=1102442861]
layout_mode = 2
text = "Display"
[node name="HSeparator4" type="HSeparator" parent="MarginContainer/VBoxContainer" unique_id=2077615507]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=1279240869]
layout_mode = 2
[node name="UseScreen" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=480029198]
unique_name_in_owner = true
layout_mode = 2
text = "Use Specific Screen:"
[node name="DisplayScreen" type="OptionButton" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=151546822]
unique_name_in_owner = true
layout_mode = 2

View file

@ -11,7 +11,7 @@ func _ready() -> void:
func _handle_run_script() -> void:
var script = GDScript.new()
script.source_code = %CodeEditor.text
script.source_code = %CodeEditor.text.replace("\t"," ")
var error := script.reload()
if error != OK:
print("Failed to parse script!")

Binary file not shown.

View file

@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://o4e6chcu01yx"
path="res://.godot/imported/Yellowtail.woff2-87764a42e0e85c9b21b77a80b21ffabf.fontdata"
[deps]
source_file="res://UI/assets/fonts/Yellowtail.woff2"
dest_files=["res://.godot/imported/Yellowtail.woff2-87764a42e0e85c9b21b77a80b21ffabf.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

BIN
UI/assets/fonts/rage.woff2 Normal file

Binary file not shown.

View file

@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://c30qqiv6sqheh"
path="res://.godot/imported/rage.woff2-9e1ac0979775e72c3e1a8a37972e5b0d.fontdata"
[deps]
source_file="res://UI/assets/fonts/rage.woff2"
dest_files=["res://.godot/imported/rage.woff2-9e1ac0979775e72c3e1a8a37972e5b0d.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -7,6 +7,53 @@ const COL_OFF := Color.LIGHT_SALMON
const SETTINGS_PANEL = preload("res://UI/settings_panel.tscn")
const SCRIPT_EDITOR = preload("res://UI/Controls/script_editor.tscn")
#region Alerts (Testing)
const FOLLOW_ALERT = preload("res://UI/Alerts/follow_alert.tscn")
const CHEER_ALERT = preload("res://UI/Alerts/cheer_alert.tscn")
const RAID_ALERT = preload("res://UI/Alerts/raid_alert.tscn")
const SUBSCRIBER_ALERT = preload("res://UI/Alerts/subscriber_alert.tscn")
var _impl_alerts: Array = [
FOLLOW_ALERT,
CHEER_ALERT,
RAID_ALERT,
SUBSCRIBER_ALERT,
]
const RANDOM_NAMES := [
"Voylin",
"meneldal",
"Pengo",
"Adam",
"Yagich",
"Falinere",
"Eroaxee",
"wolbee",
"JeSuisEmma",
"Temptic404",
"RobitussinMD",
"spimort",
"FuzzyDemonBunny",
"OdatNurd",
]
const RANDOM_MESSAGES := [
"Android was an easy fix, accidentally applied vpx to everything in Scons, Windows need an AUR package, and the arm builds are complaining about not finding vpx (probably no static build available). So not too difficult to fix luckily. But for arm I might need to build vpx myself for those builds, and whilst I'm at it I can implement support for Android as I need to clone and build it anyway for the arm builds ... Well ... that will be for later as I'll probably get stuck on this for the entire day without finishing what I started working on before XD",
"awesome, im going to run the project update tool with 4.6 and PR that so that we don't have 4.6 upgraded files in future PRs",
"I can do any time during the day or night, I can even wake up in the middle of the night if I need to",
"done 🙂 ready when you are",
"you probably been busy lately, but just curious how progress was coming along on the new launcher?",
"A version or two ago they moved the strip properties from the sequencer timeline (in blue) to the generic \"properties\" panel (in red) thats used everywhere else.",
"thanks, I realised my models were all subdivided triangle riddled messes!",
"I still don't know how to make a good short - but I can't get over the background music I was able to generate for this one. It just cracks me up. ",
"i think my favourite thing about streaming game dev is just how much of us are just sitting there guessing???",
"148 TEAM MEMBERS AND NOBODY TO WATCH ON A SUNDAY EVENING flips table",
"i'd like to formally apologize to @iRad for how i treated him in my really weird dream last night. we entered a game jam together and i spent the entire time talking to friends. he really carried me.",
"Sometimes thats all it takes, just one person believing youll succeed. Or sometimes a whole bunch of people, who mysteriously growl low if you try and leave the chair. And in no way shape or form require you to send eye blinking messages for outside help, nor throw digital snacks in one direction while sneaking out the other. ",
"That's the reason I come into the communities, to be supportive, positive, try to help when possible... because game dev is already hard enough. Sometimes all you need is just that one little boost on a rough day to keep you going and just offset the grind enough to let you feel okay again. That and giving friendly pushes to devs to get that steam page done, or that demo knocked out, or just to push forward progress.",
"Born to late to explore Earth, born to early to explore the universe",
]
#endregion
# Indicators
@onready var indicators: Control = %indicators
@onready var ind_stream: TextureRect = %ind_stream
@ -18,6 +65,7 @@ const SCRIPT_EDITOR = preload("res://UI/Controls/script_editor.tscn")
@onready var btn_run_obs: GDSubMenuButton = %btn_run_obs
@onready var btn_step_away: GDSubMenuButton = %btn_step_away
@onready var btn_tuber: GDSubMenuButton = %btn_tuber
@onready var btn_camera: GDSubMenuButton = %btn_camera
@onready var btn_chat: GDSubMenuButton = %btn_chat
@onready var btn_script_editor: GDSubMenuButton = %btn_script_editor
@onready var btn_user_list: GDSubMenuButton = %btn_user_list
@ -29,6 +77,17 @@ var anchored_position: Vector2
var _set_win: Window
var _edit_win: Window
var _obs_pid: int = -1
var _vtuber_pid: int = -1
var _vtuber_sid: int
var _camera_sid: int
func _ready() -> void:
ObsManager.obs_ready.connect(_handle_obs_connect)
func _handle_obs_connect() -> void:
_vtuber_sid = await ObsManager.get_scene_item_id("~Avatar", "~VTuber")
_camera_sid = await ObsManager.get_scene_item_id("~Avatar", "~WebCam")
func start(_main_menu_button: GDSubMenuButton = null) -> void:
anchored_position = position
@ -40,8 +99,6 @@ func start(_main_menu_button: GDSubMenuButton = null) -> void:
_connect_signals()
func start_indicators() -> void:
# TODO: Setup NO-OBS-WS Stuff
var tot = indicators.get_child_count()
for i in tot:
var ind: TextureRect = indicators.get_child(i)
@ -49,8 +106,6 @@ func start_indicators() -> void:
ind.position = (size/2 - ind.size/2)
ind.position += Vector2.from_angle(TAU*i/tot + PI/2) * (size.x/2 - ind.size.x/2)
pass
func generate_panels_buttons() -> void:
# TODO: Generate Panels?
pass
@ -105,6 +160,9 @@ func _connect_signals() -> void:
btn_settings.properly_pressed.connect(_handle_settings)
btn_script_editor.properly_pressed.connect(_handle_script_editor)
btn_run_obs.properly_pressed.connect(_handle_run_obs)
btn_step_away.properly_pressed.connect(_handle_step_away)
btn_tuber.properly_pressed.connect(_handle_run_vtuber)
btn_camera.properly_pressed.connect(_handle_camera)
func _handle_settings() -> void:
@ -141,5 +199,53 @@ func _handle_script_editor() -> void:
_edit_win = win
func _handle_run_obs() -> void:
EventManager.test_notification("Hello World!")
var tree := ProcessTree.new()
if tree.has_process_name(Globals.settings.obs_name):
print("OBS is running.")
return
else:
OS.create_process(Globals.settings.obs_path, [])
func _handle_step_away() -> void:
var alert = _impl_alerts.pick_random().instantiate()
if alert is FollowAlert:
alert.setup(RANDOM_NAMES.pick_random())
elif alert is CheerAlert:
alert.setup(RANDOM_NAMES.pick_random(), randi_range(10,1000), RANDOM_MESSAGES.pick_random())
elif alert is RaidAlert:
alert.setup(RANDOM_NAMES.pick_random(), randi_range(5,500))
elif alert is SubscriberAlert:
var prime: bool = randf() < 0.25
var months: int = randi_range(0,64) if randf() < 0.25 else 1
var tier: bool = randi_range(1,3)
var msg: String = RANDOM_MESSAGES.pick_random() if randf() < 0.45 else ""
alert.setup(RANDOM_NAMES.pick_random(), months, prime, tier, msg)
EventManager.add_alert(alert)
func _handle_run_vtuber() -> void:
var tree := ProcessTree.new()
if tree.has_process_name(Globals.settings.vtuber_name):
print("VTuber is running.")
return
else:
OS.create_process(Globals.settings.vtuber_path, [Globals.settings.vtuber_model_path])
func _handle_camera() -> void:
if not ObsManager.is_open(): return
var vtuber_vis := await ObsManager.get_item_enabled("~Avatar", _vtuber_sid)
ObsManager.set_item_enabled("~Avatar",_vtuber_sid, !vtuber_vis)
ObsManager.set_item_enabled("~Avatar",_camera_sid, vtuber_vis)
var aid := await ObsManager.get_scene_item_id("Talking", "~Avatar")
var transform := await ObsManager.get_item_transform("Talking", aid)
transform.bounds_width = 1.0
transform.bounds_height = 1.0
if vtuber_vis:
transform.position_x = 70
transform.position_y = 320
else:
transform.position_x = 39
transform.position_y = 492
print("Moving ~Avatar to: %s" % [{"x": transform.position_x, "y": transform.position_y}])
ObsManager.set_item_transform("Talking", aid, transform)
#endregion

View file

@ -180,8 +180,8 @@ metadata/_custom_type_script = "uid://8715vyt7prmd"
[node name="btn_tuber" type="Button" parent="btn_obs" unique_id=118706136]
unique_name_in_owner = true
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
offset_right = 66.0
offset_bottom = 66.0
tooltip_text = "Launch VTuber"
icon = ExtResource("12_05x4r")
script = ExtResource("8_ttodq")

View file

@ -52,7 +52,12 @@ visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="Chat Avatars" type="PanelContainer" parent="MarginContainer/VBoxContainer/TabContainer" unique_id=429108220]
[node name="Music" type="PanelContainer" parent="MarginContainer/VBoxContainer/TabContainer" unique_id=22912491]
visible = false
layout_mode = 2
metadata/_tab_index = 2
[node name="Chat Avatars" type="PanelContainer" parent="MarginContainer/VBoxContainer/TabContainer" unique_id=429108220]
visible = false
layout_mode = 2
metadata/_tab_index = 3

View file

@ -0,0 +1,7 @@
[plugin]
name="Custom Runner"
description="A plugin that runs the Godot Engine with a specific tool, such as obs-gamecapture, to capture the game window."
author="Mario Steele"
version="0.1"
script="plugin.gd"

View file

@ -0,0 +1,151 @@
@tool
extends EditorPlugin
var _runner_btn: RunnerButton
var _play_icon: Texture2D
var _stop_icon: Texture2D
func _enter_tree() -> void:
ProjectSettings.set("application/run/custom_runner", "")
ProjectSettings.add_property_info({
"name": "application/run/custom_runner",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_FILE,
"hint_string": ""
})
_runner_btn = RunnerButton.new()
add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, _runner_btn)
var cnt := _runner_btn.get_parent()
cnt.move_child(_runner_btn, cnt.get_child_count() - 3)
func _exit_tree() -> void:
remove_control_from_container(EditorPlugin.CONTAINER_TOOLBAR, _runner_btn)
_runner_btn.queue_free()
_runner_btn = null
class RunnerButton:
extends Button
var _play: Texture2D
var _stop: Texture2D
var _running_pid: int = -1
var _io: FileAccess = null
var _err: FileAccess = null
func _init() -> void:
_play = EditorInterface.get_editor_theme().get_icon(&"Play", &"EditorIcons")
_stop = EditorInterface.get_editor_theme().get_icon(&"Stop", &"EditorIcons")
shortcut = Shortcut.new()
var key := InputEventKey.new()
key.keycode = KEY_F5
key.shift_pressed = true
shortcut.events = [key]
modulate = Color.GREEN
var esb := StyleBoxEmpty.new()
add_theme_stylebox_override(&"normal", esb)
func _ready() -> void:
icon = _play
func _process(_d: float) -> void:
if _running_pid == -1: return
if not OS.is_process_running(_running_pid):
_running_pid = -1
icon = _play
modulate = Color.GREEN
func _pressed() -> void:
if _running_pid != -1:
OS.kill(_running_pid)
_running_pid = -1
icon = _play
modulate = Color.GREEN
return
var runner := ProjectSettings.get_setting("application/run/custom_runner", "")
if runner == "":
EditorInterface.play_main_scene()
return
if not FileAccess.file_exists(runner):
EditorInterface.get_editor_toaster().push_toast("Custom runner does not exist!",
EditorToaster.SEVERITY_ERROR,
"Provide the full path to the custom runner!")
return
var godot = OS.get_executable_path()
var project = ProjectSettings.globalize_path("res://").rstrip("/")
var host: String = EditorInterface.get_editor_settings().get_setting("network/debug/remote_host")
var port: int = EditorInterface.get_editor_settings().get_setting("network/debug/remote_port")
var pid: int = OS.get_process_id()
var main_scene: String = ProjectSettings.get_setting("application/run/main_scene")
var run_arguments := [
godot,
"--path", project,
"-d"
#"--remote-debug", "tcp://%s:%s" % [host, port],
#"--editor-pid", "%s" % pid,
#"--position", "320,-24",
#"--scene", main_scene
]
# --remote-debug tcp://127.0.0.1:6007 --editor-pid 3480813 --position 320,-24 --scene uid://2t2hslaaewee
print("Executing project with the following Command Line: %s %s" % [runner, run_arguments])
#_running_pid = OS.create_process(runner, run_arguments)
var res := OS.execute_with_pipe(runner, run_arguments, false)
if res.is_empty():
EditorInterface.get_editor_toaster().push_toast("Failed to Run custom runner.",EditorToaster.SEVERITY_ERROR)
_running_pid = res.pid
_io = res.stdio
_err = res.stderr
icon = _stop
modulate = Color.RED
_monitor_input_output()
func _monitor_input_output() -> void:
var output: PackedByteArray = []
var err: PackedByteArray = []
while OS.is_process_running(_running_pid):
await get_tree().process_frame
var res := _read_io(_io, output)
if res != "":
print_rich(res)
res = _read_io(_err, err)
if res != "":
print_rich(res)
await get_tree().process_frame
var str := _read_io(_io, output)
if str != "":
print(str)
str = _read_io(_err, err)
if str != "":
print(str)
_io.close()
_err.close()
_io = null
_err = null
func _read_io(io: FileAccess, buffer: PackedByteArray) -> String:
if io.get_position() < io.get_length():
buffer.append_array(io.get_buffer(io.get_length() - io.get_position()))
var nl = buffer.rfind(10)
if nl == -1:
return ""
var part := buffer.slice(0,nl)
var leftover := buffer.slice(nl+1)
buffer.clear()
buffer.append_array(leftover)
return part.get_string_from_utf8()

View file

@ -0,0 +1 @@
uid://bsdgmd1nimt6n

View file

@ -9,6 +9,9 @@ To see video files in your projects file tree, you need to add `mp4` and any oth
1. When exporting, you'll have your executable and the GDE GoZen library file, these do need to be both shared for the application to work.
2. Also, you will have to add `*.mp4` and other extension names of your video files to the resources which need to get exported for each platform you want to export for, otherwise your video files will not be included in your final export.
## Android (SAF)
Due to the way SAF works (Godot 4.6+), accessing files through the browser might give invalid paths. You need to be certain you get the absolute path instead. Inside of the `test_room` you can find an example of how this is done for Android, by basically copying the videos to a temporary folder which will give an absolute path. Only thing to be careful of is to also remove the data afterwards out of the temporary folder.
## Help needed?
You can go to the [GitHub repo](https://github.com/VoylinsGamedevJourney/gde_gozen/issues) to report problems, or visit out [Discord server](discord.gg/BdbUf7VKYC) for help/advice.

View file

@ -2,7 +2,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bijjjb1qmqf7f"
uid="uid://dcpha8teexb00"
path="res://.godot/imported/icon.svg-ac53c9c419a88eda808214a4d44243c3.ctex"
metadata={
"vram_texture": false

View file

@ -2,7 +2,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dbk4jierdjfrf"
uid="uid://bndqydg778xif"
path="res://.godot/imported/icon.webp-a5a23a9a73b8ba8140cf260e6bfffac7.ctex"
metadata={
"vram_texture": false

View file

@ -3,5 +3,5 @@
name="gde_gozen"
description="Providing performant video playback."
author="Voylin's Gamedev Journey"
version="v9.1"
version="v9.2"
script="plugin.gd"

View file

@ -107,11 +107,9 @@ func _exit_tree() -> void:
# Making certain no remaining tasks are running in separate threads.
if _video_thread != -1:
var error: int = WorkerThreadPool.wait_for_task_completion(_video_thread)
if error != OK:
printerr("Something went wrong waiting for task completion! %s" % error)
_video_thread = -1
if video != null:
close()
@ -129,7 +127,6 @@ func _ready() -> void:
func set_video_path(new_path: String) -> void:
if video != null:
close()
if !is_node_ready():
await ready
if !get_tree().root.is_node_ready():
@ -144,25 +141,26 @@ func set_video_path(new_path: String) -> void:
path = new_path
video = GoZenVideo.new()
if debug:
video.enable_debug()
else:
video.disable_debug()
_video_thread = WorkerThreadPool.add_task(_open_video)
if enable_audio:
_open_audio()
## Update the video manually by providing a GoZenVideo instance and an optional AudioStreamWAV.
func update_video(video_instance: GoZenVideo) -> void:
func update_video(video_instance: GoZenVideo, audio_stream: AudioStreamWAV = null) -> void:
if video != null:
close()
path = video_instance.get_path()
_update_video(video_instance)
_open_audio()
if audio_stream:
audio_player.stream = audio_stream
else:
_open_audio()
## Only run this function after manually having added a Video object to the `video` variable. A good reason for doing this is to load your video's at startup time to prevent your program for freezing for a second when loading in big video files. Some video formats load faster then others so if you are experiencing issues with long loading times, try to use this function and create the video object on startup, or try switching the video format which you are using.
@ -206,7 +204,6 @@ func _update_video(new_video: GoZenVideo) -> void:
image = Image.create_empty(_resolution.x, _resolution.y, false, Image.FORMAT_R8)
image.fill(Color.WHITE)
if debug:
_print_video_debug()
@ -240,7 +237,6 @@ func _set_color_profile(new_profile: COLOR_PROFILE = color_profile) -> void:
var profile_str: String = video.get_color_profile()
color_profile = new_profile
if new_profile != COLOR_PROFILE.AUTO:
profile_str = str(COLOR_PROFILE.find_key(COLOR_PROFILE.BT2100)).to_lower()
@ -248,7 +244,6 @@ func _set_color_profile(new_profile: COLOR_PROFILE = color_profile) -> void:
"bt2020", "bt2100": color_data = Vector4(1.4746, 0.16455, 0.57135, 1.8814)
"bt601", "bt470": color_data = Vector4(1.402, 0.344136, 0.714136, 1.772)
_: color_data = Vector4(1.5748, 0.1873, 0.4681, 1.8556) # bt709 and unknown
_shader_material.set_shader_parameter("color_profile", color_data)
@ -282,7 +277,6 @@ func close() -> void:
if video != null:
if is_playing:
pause()
video = null
@ -291,7 +285,6 @@ func _process(delta: float) -> void:
if is_playing:
_skips = 0
_time_elapsed += delta
if _time_elapsed < _frame_time:
return
@ -302,39 +295,36 @@ func _process(delta: float) -> void:
if current_frame >= _frame_count:
is_playing = !is_playing
if enable_audio and audio_player.stream != null:
audio_player.set_stream_paused(true)
video_ended.emit()
if loop:
seek_frame(0)
play()
else:
_sync_audio_video()
while _skips != 1:
next_frame(true)
_skips -= 1
next_frame()
elif _video_thread != -1:
var error: int = WorkerThreadPool.wait_for_task_completion(_video_thread)
if error != OK:
printerr("Something went wrong waiting for task completion! %s" % error)
_video_thread = -1
_update_video(video)
if enable_auto_play:
play()
## Start the video playback. This will play until reaching the end of the video and then pause and go back to the start.
func play() -> void:
if video != null and !is_open() and is_playing:
if not is_open():
print("The video on '%s' isn't open yet!" % path)
return
if is_playing: return
is_playing = true
if enable_audio and audio_player.stream.get_length() != 0:
@ -348,10 +338,8 @@ func play() -> void:
## Pausing the video.
func pause() -> void:
is_playing = false
if enable_audio and audio_player.stream != null:
audio_player.set_stream_paused(true)
playback_paused.emit()
@ -361,7 +349,6 @@ func _sync_audio_video() -> void:
return
elif enable_audio and audio_player.stream.get_length() != 0:
var audio_offset: float = audio_player.get_playback_position() + AudioServer.get_time_since_last_mix() - (current_frame + 1) / _frame_rate
if abs(audio_player.get_playback_position() + AudioServer.get_time_since_last_mix() - (current_frame + 1) / _frame_rate) > AUDIO_OFFSET_THRESHOLD:
if debug: print("Audio Sync: time correction: ", audio_offset)
audio_player.seek((current_frame + 1) / _frame_rate)
@ -451,7 +438,6 @@ func _set_frame_image() -> void:
RenderingServer.texture_2d_update(y_texture.get_rid(), video.get_y_data(), 0)
RenderingServer.texture_2d_update(u_texture.get_rid(), video.get_u_data(), 0)
RenderingServer.texture_2d_update(v_texture.get_rid(), video.get_v_data(), 0)
if _has_alpha:
RenderingServer.texture_2d_update(a_texture.get_rid(), video.get_a_data(), 0)
@ -463,7 +449,6 @@ func set_playback_speed(new_playback_value: float) -> void:
if enable_audio and audio_player.stream != null:
audio_player.pitch_scale = playback_speed
_set_pitch_adjust()
if is_playing:
audio_player.play(current_frame * (1.0 / _frame_rate))
@ -521,7 +506,7 @@ func _open_audio(stream_id: int = -1) -> void:
printerr("Failed to open AudioStreamFFmpeg for: %s" % path)
return
audio_player.set_stream.call_deferred(stream)
audio_player.stream = stream
func _print_stream_info(streams: PackedInt32Array) -> void:
@ -565,22 +550,19 @@ func _print_video_debug() -> void:
print("Using sws: ", video.is_using_sws())
print("Sar: ", video.get_sar())
if video_streams.size() != 0:
print_rich("Video streams: [i](%s)" % video_streams.size())
_print_stream_info(video_streams)
else:
print("No video streams found.")
print_rich("Video streams: [i](%s)" % video_streams.size())
_print_stream_info(video_streams)
if audio_streams.size() != 0:
print_rich("Audio streams: [i](%s)" % audio_streams.size())
_print_stream_info(audio_streams)
else:
elif debug:
print("No audio streams found.")
if subtitle_streams.size() != 0:
print_rich("Subtitle streams: [i](%s)" % subtitle_streams.size())
_print_stream_info(subtitle_streams)
else:
elif debug:
print("No subtitle streams found.")
if chapters.size() != 0:

View file

@ -24,10 +24,14 @@ signal event_received(event: Message)
signal _auth_required()
func is_open() -> bool:
return _ws != null and _ws.get_ready_state() == WebSocketPeer.STATE_OPEN
func connect_to_obsws(port: int, host: String = "127.0.0.1", password: String = "") -> void:
_ws = WebSocketPeer.new()
_ws.connect_to_url(WS_URL % [host,port])
_auth_required.connect(_authenticate.bind(password))
if not _auth_required.is_connected(_authenticate):
_auth_required.connect(_authenticate.bind(password))
func disconnect_from_obsws() -> void:

4
default_bus_layout.tres Normal file
View file

@ -0,0 +1,4 @@
[gd_resource type="AudioBusLayout" format=3 uid="uid://byd4lkofdecc0"]
[resource]
bus/0/volume_db = -9.632782

View file

@ -2,11 +2,25 @@ extends Node
var ol: OverlayWindow
var _queue: Array[Alert] = []
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
ol = get_tree().root.get_node("MainWin")
if get_tree().root.has_node("MainWin"):
ol = get_tree().root.get_node("MainWin")
func add_alert(alert: Alert) -> void:
if get_children().any(func(x): return x is Alert):
_queue.append(alert)
else:
add_child(alert)
func _process(_d: float) -> void:
if _queue.is_empty(): return
if get_children().any(func(x): return x is Alert):
return
var alert: Alert = _queue.pop_front()
add_child(alert)
func test_notification(msg: String) -> void:
var lbl = Label.new()

View file

@ -3,6 +3,7 @@ extends Node
var twitcher: TwitcherExtended
var context: OverlayContext
var settings: OverlaySettings
var main_win: OverlayWindow
var _pt_except: Array[Control] = []
var _pt_mouse: bool = false
var _current_pt_mask: PackedVector2Array = []

167
lib/obs_manager.gd Normal file
View file

@ -0,0 +1,167 @@
extends Node
var obs: NoOBSWS
signal obs_ready()
signal obs_disconnect()
signal stream_state_changed(state: bool)
signal input_state_changed(input_name: String, state: bool)
signal scene_changed(scene_name: String, scene_uuid: String)
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
obs = NoOBSWS.new()
add_child(obs)
obs.event_received.connect(_handle_websock_events)
obs.connection_ready.connect(obs_ready.emit)
obs.connection_closed_clean.connect(_handle_disconnect)
func is_open() -> bool:
return obs.is_open()
func connect_to_host() -> void:
if not (Globals.settings.obs_host != "" and
Globals.settings.obs_port != 0 and
Globals.settings.obs_pass != ""):
return
obs.connect_to_obsws(Globals.settings.obs_port, Globals.settings.obs_host, Globals.settings.obs_pass)
func _handle_disconnect(reason: int, message: String) -> void:
obs_disconnect.emit()
if Globals.settings.obs_reconnect:
connect_to_host()
func _handle_websock_events(event: NoOBSWS.Message) -> void:
var data := event.get_data()
if data.has("event_type"):
if not data.has("event_data"):
return
var ed: Dictionary = data.event_data
match data.event_type:
"StreamStateChanged":
stream_state_changed.emit(ed.output_active)
"InputMuteStateChanged":
input_state_changed.emit(ed.input_name, ed.input_muted)
"CurrentProgramSceneChanged":
scene_changed.emit(ed.scene_name, ed.scene_uuid)
func get_stream_status() -> bool:
var request_type := "GetStreamStatus"
var request := obs.make_generic_request(request_type)
await request.response_received
var response = request.message.get_data()
return response.response_data.output_active as bool
func get_scene_item_id(scene_name: String, source_name: String) -> int:
var request_type := "GetSceneItemId"
var request_data := {"scene_name": scene_name, "source_name": source_name}
var request := obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.scene_item_id as int
func get_item_enabled(scene_name: String, item_id: int) -> bool:
var request_type = "GetSceneItemEnabled"
var request_data = {"scene_name": scene_name, "scene_item_id": item_id}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.scene_item_enabled as bool
func set_item_enabled(scene_name: String, item_id: int, val: bool) -> void:
var request_type = "SetSceneItemEnabled"
var request_data = {"scene_name": scene_name, "scene_item_id": item_id, "scene_item_enabled": val}
obs.make_generic_request(request_type, request_data)
func get_item_transform(scene_name: String, item_id: int) -> Dictionary:
var request_type = "GetSceneItemTransform"
var request_data = {"scene_name": scene_name, "scene_item_id": item_id}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.scene_item_transform as Dictionary
func set_item_transform(scene_name: String, item_id: int, transform: Dictionary) -> void:
var request_type = "SetSceneItemTransform"
var request_data = {"scene_name": scene_name, "scene_item_id": item_id, "scene_item_transform": transform}
obs.make_generic_request(request_type, request_data)
func get_input_mute(input_name: String) -> bool:
var request_type = "GetInputMute"
var request_data = {"input_name": input_name}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.input_muted
func set_input_mute(input_name: String, val: bool) -> void:
var request_type = "SetInputMute"
var request_data = {"input_name": input_name, "input_muted": val}
obs.make_generic_request(request_type, request_data)
func get_item_filter_enabled(source_name: String, filter_name: String) -> bool:
var request_type = "GetSourceFilter"
var request_data = {
"source_name": source_name,
"filter_name": filter_name
}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.filter_enabled
func set_item_filter_enabled(source_name: String, filter_name: String, filter_enabled: bool) -> void:
var request_type = "SetSourceFilterEnabled"
var request_data = {
"source_name": source_name,
"filter_name": filter_name,
"filter_enabled": filter_enabled
}
obs.make_generic_request(request_type, request_data)
func get_item_filter_setting(source_name: String, filter_name: String) -> Dictionary:
var request_type = "GetSourceFilter"
var request_data = {
"source_name": source_name,
"filter_name": filter_name
}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
var response = request.message.get_data()
return response.response_data.filter_settings
func set_item_filter_setting(source_name: String, filter_name: String, filter_settings: Dictionary) -> void:
var request_type = "SetSourceFilterSettings"
var request_data = {
"sourceName": source_name,
"filterName": filter_name,
"filterSettings": filter_settings,
"overlay": true
}
var res: NoOBSWS.RequestResponse = obs.make_generic_request(request_type, request_data)
await res.response_received
func stop_stream() -> void:
var request_type = "StopStream"
var request_data = {}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received
func restart_media(media_name) -> void:
var request_type = "TriggerMediaInputAction"
var request_data = {"input_name": media_name, "media_action": "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART"}
var request = obs.make_generic_request(request_type, request_data)
await request.response_received

1
lib/obs_manager.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://c447ij3oc24q3

View file

@ -3,7 +3,19 @@ class_name OverlaySettings
@export var auto_connect_twitch: bool = false
@export var auto_connect_obs: bool = false
@export var obs_path: String = "/usr/bin/obs"
@export var obs_name: String = "obs"
@export var obs_host: String = "localhost"
@export var obs_port: int = 4455
@export var obs_pass: String = ""
@export var obs_reconnect: bool = false
@export var vtuber_path: String = "/home/eumario/apps/veadotube-labs-veadotube-linux-x64/veadotube.sh"
@export var vtuber_name: String = "veadotube"
@export var vtuber_model_path: String = "/home/eumario/Pictures/PNGTuber/cdw3/main_scene.veadoscene"
@export var display_screen: int = -1
@export var script_storage: Dictionary

81
lib/process_tree.gd Normal file
View file

@ -0,0 +1,81 @@
extends RefCounted
class_name ProcessTree
class ProcessInfo:
extends RefCounted
## Class representing a shallow base of Process information for a process running on the system.
## User ID that ran this process
var uid: int
## ID for the System processs
var pid: int
## Parent process that executed this ID.
var ppid: int
## Executable that was used to run this process.
var executable: String
func _init(_user: String, _pid: String, _ppid: String, name: String) -> void:
uid = _user.to_int()
pid = _pid.to_int()
ppid = _ppid.to_int()
executable = name
func _to_string() -> String:
return "<ProcessInfo: pid:%d, ppid:%d, user:%d, exec:%s>" % [pid, ppid, uid, executable]
## Returns true/false if the process is currently running
func is_running() -> bool:
return OS.is_process_running(pid)
## Kills the process on the system.
func kill() -> Error:
return OS.kill(pid)
## Kills the parent process that ran this process. (NOTE: May kill this process, or leave this process in a Zombie State)
func kill_parent() -> Error:
return OS.kill(ppid)
var processes: Array[ProcessInfo] = []
func _init() -> void:
refresh()
func refresh() -> void:
processes.clear()
var output := []
OS.execute("/usr/bin/ps", ["-eo", "uid,pid,ppid,exe"], output)
var lines: PackedStringArray = output[0].split("\n")
for line in lines:
var parts = line.split(" ",false,4)
if parts.is_empty(): continue
if parts[0] == "UID":
continue
if parts[3] == "-":
continue
var proc: ProcessInfo = ProcessInfo.new(parts[0],parts[1],parts[2],parts[3])
processes.append(proc)
func has_process_id(id: int) -> bool:
return processes.any(func(x: ProcessInfo): return x.pid == id)
func has_process_name(name: String) -> bool:
return processes.any(func(x: ProcessInfo): return x.executable == name or name in x.executable)
func get_process(id: int) -> ProcessInfo:
for proc: ProcessInfo in processes:
if proc.pid == id:
return proc
return null
func get_process_by_name(name: String) -> ProcessInfo:
for proc: ProcessInfo in processes:
if proc.executable == name or name in proc.executable:
return proc
return null
func get_all_processes_by_name(name: String) -> Array[ProcessInfo]:
var procs: Array[ProcessInfo] = []
for proc: ProcessInfo in processes:
if proc.executable == name or name in proc.executable:
procs.append(proc)
return procs

1
lib/process_tree.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://d1wr43cirxvbn

View file

@ -57,6 +57,8 @@ static var instance: TwitcherExtended
#region Public Variables
var streamer_user: TwitchUser
var bot_user: TwitchUser
var streamer_token_loaded: bool = false
var chatbot_token_loaded: bool = false
#endregion
#region Private Variables
@ -184,15 +186,19 @@ func _twitcher_setup() -> void:
#region Streamer Public Functions
func load_streamer_token() -> AuthStatus:
_log.d("Loading streamer tokens...")
streamer_token_loaded = false
var res = streamer_token._load_tokens()
if not res:
_log.d("Token doesn't exist")
return AuthStatus.UNAUTHORIZED
if streamer_token.is_token_valid():
_log.d("Token authorized")
streamer_user = await service.get_current_user()
streamer_token_loaded = true
return AuthStatus.AUTHORIZED
if streamer_token.has_refresh_token():
_log.d("Token needs refreshed")
streamer_token_loaded = true
return AuthStatus.NEEDS_REFRESH
_log.d("Token invalid")
return AuthStatus.UNAUTHORIZED
@ -244,15 +250,19 @@ func setup_streamer() -> bool:
#region Chatbot Functions
func load_chatbot_token() -> AuthStatus:
_log.d("Loading chatbot tokens...")
chatbot_token_loaded = false
var res = chatbot_token._load_tokens()
if not res:
_log.d("Token doesn't exist")
return AuthStatus.UNAUTHORIZED
if chatbot_token.is_token_valid():
_log.d("Token authroized")
bot_user = await chatbot_auth.get_user()
chatbot_token_loaded = true
return AuthStatus.AUTHORIZED
if chatbot_token.has_refresh_token():
_log.d("Token needs refreshed")
chatbot_token_loaded = true
return AuthStatus.NEEDS_REFRESH
_log.d("Token invalid")
return AuthStatus.UNAUTHORIZED

View file

@ -8,23 +8,56 @@ class_name OverlayWindow
@onready var twitcher: TwitcherExtended = $TwitcherExtended
#endregion
@export var subscriptions: Array[TwitchEventsubConfig] = []
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
Globals.main_win = self
Globals.add_children_to_passthrough_exception(floating_menu, [floating_menu.indicators])
Globals.enable_mouse_passthrough()
floating_menu.start()
Globals.twitcher = twitcher
if Globals.settings.display_screen != -1:
get_window().current_screen = Globals.settings.display_screen
var res := twitcher.load_streamer_token()
if res == TwitcherExtended.AuthStatus.UNAUTHORIZED:
return
if Globals.settings.auto_connect_twitch:
var res := await twitcher.load_streamer_token()
if res == TwitcherExtended.AuthStatus.UNAUTHORIZED:
return
func _handle_twitch_auth() -> void:
await twitcher.setup_streamer()
#if twitcher.is_streamer_authed():
#auth_twitch.disabled = true
if await twitcher.setup_streamer():
for sub in subscriptions:
if sub.condition.has(&"broadcaster_user_id"):
sub.condition.broadcaster_user_id = twitcher.streamer_user.id
if sub.condition.has(&"moderator_user_id"):
sub.condition.moderator_user_id = twitcher.streamer_user.id
if sub.condition.has(&"to_broadcaster_user_id"):
sub.condition.to_broadcaster_user_id = twitcher.streamer_user.id
func _handle_bot_auth() -> void:
await twitcher.setup_chatbot()
#if twitcher.is_chatbot_authed():
#auth_bot.disabled = true
twitcher.subscribe_event(sub.definition, sub.condition)
res = await twitcher.load_chatbot_token()
if res == TwitcherExtended.AuthStatus.UNAUTHORIZED:
return
twitcher.setup_chatbot()
ObsManager.scene_changed.connect(_handle_scene_changed)
if Globals.settings.auto_connect_obs:
ObsManager.connect_to_host()
pass
func _handle_scene_changed(scene_name: String, scene_uuid: String) -> void:
match scene_name:
"StartingSoon":
%ChatBox.dock = ChatBox.ChatDock.RIGHT
"Talking":
%ChatBox.dock = ChatBox.ChatDock.RIGHT
"Desktop":
%ChatBox.dock = ChatBox.ChatDock.BOTTOM
"Gaming":
%ChatBox.dock = ChatBox.ChatDock.RIGHT
"BRB":
%ChatBox.dock = ChatBox.ChatDock.RIGHT
"EndingSoon":
%ChatBox.dock = ChatBox.ChatDock.RIGHT

View file

@ -18,15 +18,118 @@
[ext_resource type="Texture2D" uid="uid://g1dbcjksbotw" path="res://addons/twitcher/assets/fallback_texture.tres" id="13_l81bt"]
[ext_resource type="Script" uid="uid://6v8jnfjwbnhm" path="res://addons/twitcher/media/twitch_image_transformer.gd" id="14_dr7ot"]
[ext_resource type="Script" uid="uid://iv0mgv0lu8b0" path="res://addons/twitcher/auth/twitch_auth.gd" id="14_l81bt"]
[ext_resource type="Script" uid="uid://cjug64e3433g0" path="res://addons/twitcher/eventsub/twitch_eventsub_config.gd" id="14_vifpv"]
[ext_resource type="Resource" uid="uid://b8735fmqjt8up" path="res://addons/twitcher/chat/twitch_bot_scopes.tres" id="15_dr7ot"]
[ext_resource type="Script" uid="uid://bf0wi70haua35" path="res://addons/twitcher/lib/oOuch/oauth.gd" id="16_m7rpr"]
[ext_resource type="Script" uid="uid://blnbogtrshw4r" path="res://addons/twitcher/auth/twitch_token_handler.gd" id="17_lpeoe"]
[ext_resource type="Script" uid="uid://csq2hmf1bsgku" path="res://lib/twitcher_extended/chatbot_authorization.gd" id="18_vifpv"]
[ext_resource type="PackedScene" uid="uid://djdxpwrmlrsp8" path="res://ChatAvatars/CatSlimes/cat_world.tscn" id="23_7nx26"]
[ext_resource type="PackedScene" uid="uid://cstq30mjx0pch" path="res://UI/Controls/chat_box.tscn" id="23_lpeoe"]
[ext_resource type="Script" uid="uid://cv06lthuq2c2o" path="res://general_commands.gd" id="24_dr7ot"]
[ext_resource type="Script" uid="uid://bmluckfvgm1c2" path="res://addons/twitcher/chat/twitch_command.gd" id="24_l81bt"]
[ext_resource type="PackedScene" uid="uid://bndydwe6rrb6p" path="res://UI/floating_menu.tscn" id="26_m7rpr"]
[sub_resource type="Resource" id="Resource_1scbk"]
script = ExtResource("14_vifpv")
type = 15
condition = {
&"broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_dqy2v"]
script = ExtResource("14_vifpv")
type = 17
condition = {
&"broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_7ft7d"]
script = ExtResource("14_vifpv")
type = 18
condition = {
&"broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_84aqx"]
script = ExtResource("14_vifpv")
type = 5
condition = {
&"broadcaster_user_id": "",
&"moderator_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_es602"]
script = ExtResource("14_vifpv")
type = 20
condition = {
&"to_broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_s0vyq"]
script = ExtResource("14_vifpv")
type = 21
condition = {
&"broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_vwie2"]
script = ExtResource("14_vifpv")
type = 37
condition = {
&"broadcaster_user_id": "",
&"reward_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_qp6p1"]
script = ExtResource("14_vifpv")
type = 38
condition = {
&"broadcaster_user_id": "",
&"reward_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_2a87i"]
script = ExtResource("14_vifpv")
type = 64
condition = {
&"broadcaster_user_id": "",
&"moderator_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_do4qx"]
script = ExtResource("14_vifpv")
type = 65
condition = {
&"broadcaster_user_id": "",
&"moderator_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_dgy7a"]
script = ExtResource("14_vifpv")
type = 6
condition = {
&"broadcaster_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_qh6mc"]
script = ExtResource("14_vifpv")
condition = {
&"broadcaster_user_id": "",
&"moderator_user_id": ""
}
metadata/_custom_type_script = "uid://cjug64e3433g0"
[sub_resource type="Resource" id="Resource_m7rpr"]
script = ExtResource("14_dr7ot")
@ -52,6 +155,11 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_bgu0c")
subscriptions = Array[ExtResource("14_vifpv")]([SubResource("Resource_1scbk"), SubResource("Resource_dqy2v"), SubResource("Resource_7ft7d"), SubResource("Resource_84aqx"), SubResource("Resource_es602"), SubResource("Resource_s0vyq"), SubResource("Resource_vwie2"), SubResource("Resource_qp6p1"), SubResource("Resource_2a87i"), SubResource("Resource_do4qx"), SubResource("Resource_dgy7a"), SubResource("Resource_qh6mc")])
[node name="ChatBox" parent="." unique_id=859214320 instance=ExtResource("23_lpeoe")]
unique_name_in_owner = true
layout_mode = 1
[node name="TwitcherExtended" type="Node" parent="." unique_id=1225944144 node_paths=PackedStringArray("service", "chat", "bot", "chatbot_auth", "eventsub", "api", "auth", "media")]
script = ExtResource("2_x1kh3")

View file

@ -15,12 +15,14 @@ config/description="Enter an interesting project description here!"
run/main_scene="uid://2t2hslaaewee"
config/features=PackedStringArray("4.6")
config/icon="res://icon.png"
run/custom_runner=""
[autoload]
Globals="*uid://cuap0k5jagdtj"
ChatManager="*uid://ct1s5eymb8mns"
EventManager="*uid://cvjhecj1me4xl"
ObsManager="*uid://c447ij3oc24q3"
[display]

94
tools/pt_testing.gd Normal file
View file

@ -0,0 +1,94 @@
@tool
extends EditorScript
var finds: Array = [
"kitty",
"/opt/zen-browser-bin/zen-bin",
"zen-bin",
"bin/dolphin",
"Discord",
"discord"
]
class ProcessInfoTest:
extends RefCounted
var user: int
var pid: int
var ppid: int
var executable: String
func _init(_user: String, _pid: String, _ppid: String, name: String) -> void:
user = _user.to_int()
pid = _pid.to_int()
ppid = _ppid.to_int()
executable = name
func _to_string() -> String:
return "<ProcessInfo: pid:%d, ppid:%d, user:%d, exec:%s>" % [pid, ppid, user, executable]
func is_running() -> bool:
return OS.is_process_running(pid)
func kill() -> Error:
return OS.kill(pid)
func kill_parent() -> Error:
return OS.kill(ppid)
class ProcessTreeTest:
extends RefCounted
var processes: Array[ProcessInfoTest] = []
func _init() -> void:
refresh()
func refresh() -> void:
processes.clear()
var output := []
OS.execute("/usr/bin/ps", ["-eo", "uid,pid,ppid,exe"], output)
var lines: PackedStringArray = output[0].split("\n")
for line in lines:
var parts = line.split(" ",false,4)
if parts[0] == "UID":
continue
if parts[3] == "-":
continue
var proc: ProcessInfoTest = ProcessInfoTest.new(parts[0],parts[1],parts[2],parts[3])
processes.append(proc)
func has_process_id(id: int) -> bool:
return processes.any(func(x: ProcessInfoTest): return x.pid == id)
func has_process_name(name: String) -> bool:
return processes.any(func(x: ProcessInfoTest): return x.executable == name or name in x.executable)
func get_process(id: int) -> ProcessInfoTest:
for proc: ProcessInfoTest in processes:
if proc.pid == id:
return proc
return null
func get_process_by_name(name: String) -> ProcessInfoTest:
for proc: ProcessInfoTest in processes:
if proc.executable == name or name in proc.executable:
return proc
return null
func get_all_processes_by_name(name: String) -> Array[ProcessInfoTest]:
var procs: Array[ProcessInfoTest] = []
for proc: ProcessInfoTest in processes:
if proc.executable == name or name in proc.executable:
procs.append(proc)
return procs
# Called when the script is executed (using File -> Run in Script Editor).
func _run() -> void:
var tree := ProcessTreeTest.new()
for name in finds:
var has_proc := tree.has_process_name(name)
var proc := tree.get_process_by_name(name)
var procs := tree.get_all_processes_by_name(name)
print("Has Process '%s': %s" % [name, has_proc])
print("First Process '%s': %s" % [name, proc])
print("Processes '%s': %s" % [name, procs])
print("")

1
tools/pt_testing.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://b6xjdxwi4o27f