From cfc0ed64ee1298ab172f78c1217157e5292f2e5f Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 10 May 2025 20:20:33 +0930 Subject: [PATCH 01/23] Create Multicast DNS Service - Issues with address reuse. Will try swap it with a UDP server. --- Mods/VRChatOSC/VRChatOSC.gd | 14 ++ Mods/VRChatOSC/VRChatOSC.gd.uid | 1 + Mods/VRChatOSC/VRChatOSC.tscn | 13 ++ .../godot-multicast-dns/MulticastDNS.gd | 196 ++++++++++++++++++ .../godot-multicast-dns/MulticastDNS.gd.uid | 1 + Mods/VRChatOSC/godot-multicast-dns/README.md | 8 + 6 files changed, 233 insertions(+) create mode 100644 Mods/VRChatOSC/VRChatOSC.gd create mode 100644 Mods/VRChatOSC/VRChatOSC.gd.uid create mode 100644 Mods/VRChatOSC/VRChatOSC.tscn create mode 100644 Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd create mode 100644 Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd.uid create mode 100644 Mods/VRChatOSC/godot-multicast-dns/README.md diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd new file mode 100644 index 0000000..d4d3bf3 --- /dev/null +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -0,0 +1,14 @@ +extends Mod_Base +class_name VRChatOSC + +@export var dns_service : MulticastDNS + +func _ready() -> void: + dns_service.on_receive.connect(_dns_packet) + +func _process(_delta : float) -> void: + pass + +func _dns_packet(packet : MulticastDNS.DNSPacket, raw_packet : StreamPeerBuffer) -> void: + pass + diff --git a/Mods/VRChatOSC/VRChatOSC.gd.uid b/Mods/VRChatOSC/VRChatOSC.gd.uid new file mode 100644 index 0000000..e472d3a --- /dev/null +++ b/Mods/VRChatOSC/VRChatOSC.gd.uid @@ -0,0 +1 @@ +uid://ysgfrvghy1n5 diff --git a/Mods/VRChatOSC/VRChatOSC.tscn b/Mods/VRChatOSC/VRChatOSC.tscn new file mode 100644 index 0000000..0353d81 --- /dev/null +++ b/Mods/VRChatOSC/VRChatOSC.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=3 format=3 uid="uid://cpe3ulnjnrapo"] + +[ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatOSC.gd" id="1_x840n"] +[ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_b4fua"] + +[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service")] +script = ExtResource("1_x840n") +dns_service = NodePath("MulticastDNS") +metadata/_custom_type_script = "uid://bsjotf2wtnmeq" + +[node name="MulticastDNS" type="Node" parent="."] +script = ExtResource("2_b4fua") +metadata/_custom_type_script = "uid://bfemeu7ysbxwc" diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd new file mode 100644 index 0000000..e71139d --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -0,0 +1,196 @@ +extends Node +class_name MulticastDNS + +class DNSPacket: + ## Unsigned Short - ID + var id : int + ## QUERYRESPONSE + var query_response : bool + ## Integer - OPCODE + var opcode : int + ## CONFLICT + var conflict : bool + ## TRUNCATION + var truncation : bool + ## TENTATIVE + var tentative : bool + ## Integer - RESPONSECODE + var response_code : int + + ## DNS Questions + var dns_questions : Array[DNSQuestion] = [] + ## DNS Answers (if any) + var dns_answers : Array[DNSRecord] = [] + ## DNS Authoritories + var dns_authoritories : Array[DNSRecord] = [] + ## DNS Additional + var dns_additional : Array[DNSRecord] = [] + + func _init(packet : StreamPeerBuffer) -> void: + id = packet.get_u16() + var flags = packet.read_u16() + response_code = (flags & 0x000F); + tentative = (flags & 0x0100) == 0x0100; + truncation = (flags & 0x0200) == 0x0200; + conflict = (flags & 0x0400) == 0x0400; + opcode = (flags & 0x7800) >> 11; + query_response = (flags & 0x8000) == 0x8000 + + # Fill in the extra properties based on lengths read from packet. + var question_length = packet.get_u16() + var answer_length = packet.get_u16() + var auth_length = packet.get_u16() + var add_length = packet.get_u16() + for i in range(question_length): + dns_questions.append(DNSQuestion.new(packet)) + for i in range(question_length): + dns_answers.append(DNSRecord.new(packet)) + for i in range(question_length): + dns_authoritories.append(DNSRecord.new(packet)) + for i in range(question_length): + dns_additional.append(DNSRecord.new(packet)) + +class DNSQuestion: + ## Unsigned Short - Type of question/record. + var dns_type : RECORD_TYPE + ## Unsigned Short - The question/record class. + var dns_class : int + ## Labels for the question + var labels : Array[String] = [] + var _cache : Dictionary = {} + + ## Initialize the properties. + func _init(packet : StreamPeerBuffer) -> void: + labels = _read_labels(packet) + dns_type = packet.get_u16() + dns_class = packet.get_u16() + + ## Recursively read all labels + func _read_labels(packet : StreamPeerBuffer) -> Array[String]: + var pos = packet.get_position() + var length = packet.get_8() + if length & 0xC0 == 0xC0: + var pointer = (length ^ 0xC0) << 8 | packet.get_u8() + var cname = _cache[pointer] + _cache[pos] = cname + var inner_labels : Array[String] = [] + if length == 0: + return inner_labels + var raw_data = packet.get_data(length) + var packed_data = PackedByteArray(raw_data) + + inner_labels.append(packed_data.get_string_from_utf8()) + inner_labels.append_array(_read_labels(packet)) + _cache[pos] = inner_labels + + return inner_labels + +enum RECORD_TYPE { + A = 1, + NS = 2, + PTR = 12, + TXT = 16, + SRV = 33 +} + +class DNSRecord: + extends DNSQuestion + + ## Time-to-live in seconds + var ttl_seconds : int + ## Length of data + var length : int + ## Structured data (changes depending on record_type) + var data : Dictionary + + ## Read from the packet to initialize the DNS Record + func _init(packet : StreamPeerBuffer) -> void: + # Make sure we init the details of the packet. + super(packet) + + ttl_seconds = packet.get_u32() + length = packet.get_u16() + + if dns_type == RECORD_TYPE.A: + _a_record(packet) + elif dns_type == RECORD_TYPE.PTR: + _ptr_record(packet) + elif dns_type == RECORD_TYPE.SRV: + _srv_record(packet) + elif dns_type == RECORD_TYPE.NS: + _ns_record(packet) + elif dns_type == RECORD_TYPE.TXT: + _txt_record(packet) + else: + print("Unsupported DNS record type found: %s", dns_type) + + func _a_record(packet : StreamPeerBuffer) -> void: + data["address"] = _get_ipv4_address(packet) + func _ptr_record(packet : StreamPeerBuffer) -> void: + data["domain_labels"] = _read_labels(packet) + func _srv_record(packet : StreamPeerBuffer) -> void: + data["priority"] = packet.get_u16() + data["weight"] = packet.get_u16() + data["port"] = packet.get_u16() + data["target"] = _read_labels(packet) + func _ns_record(packet : StreamPeerBuffer) -> void: + data["authority"] = _read_labels(packet) + func _txt_record(packet : StreamPeerBuffer) -> void: + data["text"] = "" + var l = length + while l > 0: + var part_length : int = packet.get_u8() + var part : PackedByteArray = PackedByteArray(packet.get_data(part_length)) + var str_part : String = part.get_string_from_ascii() + data["text"] += str_part + l -= part_length + 1 # We +1 here for the part length byte we read. + func _get_ipv4_address(packet : StreamPeerBuffer) -> String: + # WHY + var ip = packet.get_u32() + var ip_bytes : Array[int] = [0, 0, 0, 0] + ip_bytes[3] = ip & 0xFF + ip_bytes[2] = ip >> 8 & 0xFF + ip_bytes[1] = ip >> 16 & 0xFF + ip_bytes[0] = ip >> 24 && 0xFF + return "%s.%s.%s.%s" % [ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]] + +var receiver : PacketPeerUDP +var multicast_address : String = "224.0.0.251" +var local_addresses : Array[String] = [] + +signal on_receive(packet : DNSPacket, raw_packet : StreamPeerBuffer) + +func _ready() -> void: + receiver = PacketPeerUDP.new() + # We only listen on ipv4, we're not using mDNS for IPv6. + var err = receiver.bind(5353, "0.0.0.0") + for interface_details : Dictionary in IP.get_local_interfaces(): + receiver.join_multicast_group(multicast_address, interface_details["name"]) + # TODO: Make sender sockets for each local interface to support sending. + for ip_addr in interface_details["addresses"]: + if local_addresses.has(ip_addr): + continue + local_addresses.append(ip_addr) + if err > 0: + pass +func _process(delta : float) -> void: + if receiver.get_available_packet_count() <= 0: + return + + var packet_bytes : PackedByteArray = receiver.get_packet() + + # Make sure it is local, this may be disregarded in some situations in the future? + # FIXME: If issues happen, remove this check. + if not local_addresses.has(receiver.get_packet_ip()): + return + + # Packet is big endian. Little endian is all that the extension methods of PackedByteArray support. + # We must use a StreamPeerBuffer. + # Source: https://github.com/godotengine/godot-proposals/issues/9586#issuecomment-2074227585 + var packet : StreamPeerBuffer = StreamPeerBuffer.new() + packet.data_array = packet_bytes + packet.big_endian = true + + var dns_packet : DNSPacket = DNSPacket.new(packet) + + on_receive.emit(dns_packet, packet) diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd.uid b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd.uid new file mode 100644 index 0000000..c2480bc --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd.uid @@ -0,0 +1 @@ +uid://bfemeu7ysbxwc diff --git a/Mods/VRChatOSC/godot-multicast-dns/README.md b/Mods/VRChatOSC/godot-multicast-dns/README.md new file mode 100644 index 0000000..45820f8 --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/README.md @@ -0,0 +1,8 @@ +# Description +This library implements Multicast DNS (MDNS) functionality, primarily receiving MDNS responses. + +# Features +Interprets query responses and allows easy access. + +# TODO +Sending queries From 3d9d60c57846ed6fbf7752d525559848cb284398 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 10 May 2025 21:18:28 +0930 Subject: [PATCH 02/23] Fix various bugs - Changed to UDPServer to ensure it listens to 5353. - Various mistakes made in implementing labels have been corrected. - Cache for labels is across all packets, instead of per DNSRecord. --- .../godot-multicast-dns/MulticastDNS.gd | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index e71139d..d68a6f2 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -28,7 +28,7 @@ class DNSPacket: func _init(packet : StreamPeerBuffer) -> void: id = packet.get_u16() - var flags = packet.read_u16() + var flags = packet.get_u16() response_code = (flags & 0x000F); tentative = (flags & 0x0100) == 0x0100; truncation = (flags & 0x0200) == 0x0200; @@ -36,19 +36,21 @@ class DNSPacket: opcode = (flags & 0x7800) >> 11; query_response = (flags & 0x8000) == 0x8000 + var cache : Dictionary = {} + # Fill in the extra properties based on lengths read from packet. var question_length = packet.get_u16() var answer_length = packet.get_u16() var auth_length = packet.get_u16() var add_length = packet.get_u16() for i in range(question_length): - dns_questions.append(DNSQuestion.new(packet)) - for i in range(question_length): - dns_answers.append(DNSRecord.new(packet)) - for i in range(question_length): - dns_authoritories.append(DNSRecord.new(packet)) - for i in range(question_length): - dns_additional.append(DNSRecord.new(packet)) + dns_questions.append(DNSQuestion.new(packet, cache)) + for i in range(answer_length): + dns_answers.append(DNSRecord.new(packet, cache)) + for i in range(auth_length): + dns_authoritories.append(DNSRecord.new(packet, cache)) + for i in range(add_length): + dns_additional.append(DNSRecord.new(packet, cache)) class DNSQuestion: ## Unsigned Short - Type of question/record. @@ -57,10 +59,11 @@ class DNSQuestion: var dns_class : int ## Labels for the question var labels : Array[String] = [] - var _cache : Dictionary = {} - + ## The cache MUST be the same across the entire packet deserialization. + var _cache : Dictionary ## Initialize the properties. - func _init(packet : StreamPeerBuffer) -> void: + func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: + _cache = cache labels = _read_labels(packet) dns_type = packet.get_u16() dns_class = packet.get_u16() @@ -68,15 +71,19 @@ class DNSQuestion: ## Recursively read all labels func _read_labels(packet : StreamPeerBuffer) -> Array[String]: var pos = packet.get_position() - var length = packet.get_8() + var length = packet.get_u8() + # Check if compressed. if length & 0xC0 == 0xC0: var pointer = (length ^ 0xC0) << 8 | packet.get_u8() var cname = _cache[pointer] _cache[pos] = cname + return cname var inner_labels : Array[String] = [] if length == 0: return inner_labels - var raw_data = packet.get_data(length) + + # Get data returns a record of the attempt, and the results from the attempt. + var raw_data = packet.get_data(length)[1] var packed_data = PackedByteArray(raw_data) inner_labels.append(packed_data.get_string_from_utf8()) @@ -104,9 +111,9 @@ class DNSRecord: var data : Dictionary ## Read from the packet to initialize the DNS Record - func _init(packet : StreamPeerBuffer) -> void: + func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: # Make sure we init the details of the packet. - super(packet) + super(packet, cache) ttl_seconds = packet.get_u32() length = packet.get_u16() @@ -140,7 +147,7 @@ class DNSRecord: var l = length while l > 0: var part_length : int = packet.get_u8() - var part : PackedByteArray = PackedByteArray(packet.get_data(part_length)) + var part : PackedByteArray = PackedByteArray(packet.get_data(part_length)[1]) var str_part : String = part.get_string_from_ascii() data["text"] += str_part l -= part_length + 1 # We +1 here for the part length byte we read. @@ -148,49 +155,59 @@ class DNSRecord: # WHY var ip = packet.get_u32() var ip_bytes : Array[int] = [0, 0, 0, 0] - ip_bytes[3] = ip & 0xFF - ip_bytes[2] = ip >> 8 & 0xFF - ip_bytes[1] = ip >> 16 & 0xFF - ip_bytes[0] = ip >> 24 && 0xFF - return "%s.%s.%s.%s" % [ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]] + ip_bytes[0] = int((ip >> 24) & 0xFF) + ip_bytes[1] = int((ip >> 16) & 0xFF) + ip_bytes[2] = int((ip >> 8) & 0XFF) + ip_bytes[3] = int(ip & 0xFF) + + return "%d.%d.%d.%d" % [ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]] -var receiver : PacketPeerUDP +var server : UDPServer +var clients : Array[PacketPeerUDP] = [] var multicast_address : String = "224.0.0.251" var local_addresses : Array[String] = [] signal on_receive(packet : DNSPacket, raw_packet : StreamPeerBuffer) func _ready() -> void: - receiver = PacketPeerUDP.new() + server = UDPServer.new() # We only listen on ipv4, we're not using mDNS for IPv6. - var err = receiver.bind(5353, "0.0.0.0") - for interface_details : Dictionary in IP.get_local_interfaces(): - receiver.join_multicast_group(multicast_address, interface_details["name"]) - # TODO: Make sender sockets for each local interface to support sending. - for ip_addr in interface_details["addresses"]: - if local_addresses.has(ip_addr): - continue - local_addresses.append(ip_addr) + var err = server.listen(5353, "0.0.0.0") + if err > 0: pass func _process(delta : float) -> void: - if receiver.get_available_packet_count() <= 0: - return + server.poll() # Important! + if server.is_connection_available(): + var receiver = server.take_connection() + for interface_details : Dictionary in IP.get_local_interfaces(): + receiver.join_multicast_group(multicast_address, interface_details["name"]) + # TODO: Make sender sockets for each local interface to support sending. + for ip_addr in interface_details["addresses"]: + if local_addresses.has(ip_addr): + continue + local_addresses.append(ip_addr) + clients.append(receiver) + + for receiver in clients: + if receiver.get_available_packet_count() <= 0: + continue + + var packet_bytes : PackedByteArray = receiver.get_packet() - var packet_bytes : PackedByteArray = receiver.get_packet() - - # Make sure it is local, this may be disregarded in some situations in the future? - # FIXME: If issues happen, remove this check. - if not local_addresses.has(receiver.get_packet_ip()): - return - - # Packet is big endian. Little endian is all that the extension methods of PackedByteArray support. - # We must use a StreamPeerBuffer. - # Source: https://github.com/godotengine/godot-proposals/issues/9586#issuecomment-2074227585 - var packet : StreamPeerBuffer = StreamPeerBuffer.new() - packet.data_array = packet_bytes - packet.big_endian = true - - var dns_packet : DNSPacket = DNSPacket.new(packet) - - on_receive.emit(dns_packet, packet) + # Make sure it is local, this may be disregarded in some situations in the future? + # FIXME: If issues happen, remove this check. + var packet_ip = receiver.get_packet_ip() + if not local_addresses.has(receiver.get_packet_ip()): + continue + + # Packet is big endian. Little endian is all that the extension methods of PackedByteArray support. + # We must use a StreamPeerBuffer. + # Source: https://github.com/godotengine/godot-proposals/issues/9586#issuecomment-2074227585 + var packet : StreamPeerBuffer = StreamPeerBuffer.new() + packet.data_array = packet_bytes + packet.big_endian = true + + var dns_packet : DNSPacket = DNSPacket.new(packet) + + on_receive.emit(dns_packet, packet) From 4138b28f9faee2d1ab9556b655d84b57ad206d99 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 10 May 2025 21:34:22 +0930 Subject: [PATCH 03/23] Refactor to other scripts --- Mods/VRChatOSC/VRChatOSC.gd | 2 +- .../godot-multicast-dns/DNSPacket.gd | 52 ++++++ .../godot-multicast-dns/DNSPacket.gd.uid | 1 + .../godot-multicast-dns/DNSQuestion.gd | 40 +++++ .../godot-multicast-dns/DNSQuestion.gd.uid | 1 + .../godot-multicast-dns/DNSRecord.gd | 69 ++++++++ .../godot-multicast-dns/DNSRecord.gd.uid | 1 + .../godot-multicast-dns/MulticastDNS.gd | 165 +----------------- 8 files changed, 166 insertions(+), 165 deletions(-) create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd.uid create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd.uid create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd create mode 100644 Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd.uid diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index d4d3bf3..0c167ee 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -9,6 +9,6 @@ func _ready() -> void: func _process(_delta : float) -> void: pass -func _dns_packet(packet : MulticastDNS.DNSPacket, raw_packet : StreamPeerBuffer) -> void: +func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: pass diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd new file mode 100644 index 0000000..abfd5d6 --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd @@ -0,0 +1,52 @@ +extends Node +class_name DNSPacket + +## Unsigned Short - ID +var id : int +## QUERYRESPONSE +var query_response : bool +## Integer - OPCODE +var opcode : int +## CONFLICT +var conflict : bool +## TRUNCATION +var truncation : bool +## TENTATIVE +var tentative : bool +## Integer - RESPONSECODE +var response_code : int + +## DNS Questions +var dns_questions : Array[DNSQuestion] = [] +## DNS Answers (if any) +var dns_answers : Array[DNSRecord] = [] +## DNS Authoritories +var dns_authoritories : Array[DNSRecord] = [] +## DNS Additional +var dns_additional : Array[DNSRecord] = [] + +func _init(packet : StreamPeerBuffer) -> void: + id = packet.get_u16() + var flags = packet.get_u16() + response_code = (flags & 0x000F); + tentative = (flags & 0x0100) == 0x0100; + truncation = (flags & 0x0200) == 0x0200; + conflict = (flags & 0x0400) == 0x0400; + opcode = (flags & 0x7800) >> 11; + query_response = (flags & 0x8000) == 0x8000 + + var cache : Dictionary = {} + + # Fill in the extra properties based on lengths read from packet. + var question_length = packet.get_u16() + var answer_length = packet.get_u16() + var auth_length = packet.get_u16() + var add_length = packet.get_u16() + for i in range(question_length): + dns_questions.append(DNSQuestion.new(packet, cache)) + for i in range(answer_length): + dns_answers.append(DNSRecord.new(packet, cache)) + for i in range(auth_length): + dns_authoritories.append(DNSRecord.new(packet, cache)) + for i in range(add_length): + dns_additional.append(DNSRecord.new(packet, cache)) diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd.uid b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd.uid new file mode 100644 index 0000000..de80aaf --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd.uid @@ -0,0 +1 @@ +uid://cwfmaaoc0u1ig diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd new file mode 100644 index 0000000..63bf11d --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd @@ -0,0 +1,40 @@ +extends Node +class_name DNSQuestion +## Unsigned Short - Type of question/record. +var dns_type : int +## Unsigned Short - The question/record class. +var dns_class : int +## Labels for the question +var labels : Array[String] = [] +## The cache MUST be the same across the entire packet deserialization. +var _cache : Dictionary +## Initialize the properties. +func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: + _cache = cache + labels = _read_labels(packet) + dns_type = packet.get_u16() + dns_class = packet.get_u16() + +## Recursively read all labels +func _read_labels(packet : StreamPeerBuffer) -> Array[String]: + var pos = packet.get_position() + var length = packet.get_u8() + # Check if compressed. + if length & 0xC0 == 0xC0: + var pointer = (length ^ 0xC0) << 8 | packet.get_u8() + var cname = _cache[pointer] + _cache[pos] = cname + return cname + var inner_labels : Array[String] = [] + if length == 0: + return inner_labels + + # Get data returns a record of the attempt, and the results from the attempt. + var raw_data = packet.get_data(length)[1] + var packed_data = PackedByteArray(raw_data) + + inner_labels.append(packed_data.get_string_from_utf8()) + inner_labels.append_array(_read_labels(packet)) + _cache[pos] = inner_labels + + return inner_labels diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd.uid b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd.uid new file mode 100644 index 0000000..2ada4d1 --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd.uid @@ -0,0 +1 @@ +uid://d25h1nb2rddvb diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd new file mode 100644 index 0000000..7e379e0 --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -0,0 +1,69 @@ +class_name DNSRecord +extends DNSQuestion + +enum RECORD_TYPE { + A = 1, + NS = 2, + PTR = 12, + TXT = 16, + SRV = 33 +} + +## Time-to-live in seconds +var ttl_seconds : int +## Length of data +var length : int +## Structured data (changes depending on record_type) +var data : Dictionary + +## Read from the packet to initialize the DNS Record +func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: + # Make sure we init the details of the packet. + super(packet, cache) + + ttl_seconds = packet.get_u32() + length = packet.get_u16() + + if dns_type == RECORD_TYPE.A: + _a_record(packet) + elif dns_type == RECORD_TYPE.PTR: + _ptr_record(packet) + elif dns_type == RECORD_TYPE.SRV: + _srv_record(packet) + elif dns_type == RECORD_TYPE.NS: + _ns_record(packet) + elif dns_type == RECORD_TYPE.TXT: + _txt_record(packet) + else: + print("Unsupported DNS record type found: %s", dns_type) + +func _a_record(packet : StreamPeerBuffer) -> void: + data["address"] = _get_ipv4_address(packet) +func _ptr_record(packet : StreamPeerBuffer) -> void: + data["domain_labels"] = _read_labels(packet) +func _srv_record(packet : StreamPeerBuffer) -> void: + data["priority"] = packet.get_u16() + data["weight"] = packet.get_u16() + data["port"] = packet.get_u16() + data["target"] = _read_labels(packet) +func _ns_record(packet : StreamPeerBuffer) -> void: + data["authority"] = _read_labels(packet) +func _txt_record(packet : StreamPeerBuffer) -> void: + data["text"] = "" + var l = length + while l > 0: + var part_length : int = packet.get_u8() + var part : PackedByteArray = PackedByteArray(packet.get_data(part_length)[1]) + var str_part : String = part.get_string_from_ascii() + data["text"] += str_part + l -= part_length + 1 # We +1 here for the part length byte we read. +func _get_ipv4_address(packet : StreamPeerBuffer) -> String: + # WHY + var ip = packet.get_u32() + var ip_bytes : Array[int] = [0, 0, 0, 0] + ip_bytes[0] = int((ip >> 24) & 0xFF) + ip_bytes[1] = int((ip >> 16) & 0xFF) + ip_bytes[2] = int((ip >> 8) & 0XFF) + ip_bytes[3] = int(ip & 0xFF) + + return "%d.%d.%d.%d" % [ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]] diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd.uid b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd.uid new file mode 100644 index 0000000..829601d --- /dev/null +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd.uid @@ -0,0 +1 @@ +uid://cj77u4poxfa1x diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index d68a6f2..98a80ae 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -1,167 +1,6 @@ extends Node class_name MulticastDNS -class DNSPacket: - ## Unsigned Short - ID - var id : int - ## QUERYRESPONSE - var query_response : bool - ## Integer - OPCODE - var opcode : int - ## CONFLICT - var conflict : bool - ## TRUNCATION - var truncation : bool - ## TENTATIVE - var tentative : bool - ## Integer - RESPONSECODE - var response_code : int - - ## DNS Questions - var dns_questions : Array[DNSQuestion] = [] - ## DNS Answers (if any) - var dns_answers : Array[DNSRecord] = [] - ## DNS Authoritories - var dns_authoritories : Array[DNSRecord] = [] - ## DNS Additional - var dns_additional : Array[DNSRecord] = [] - - func _init(packet : StreamPeerBuffer) -> void: - id = packet.get_u16() - var flags = packet.get_u16() - response_code = (flags & 0x000F); - tentative = (flags & 0x0100) == 0x0100; - truncation = (flags & 0x0200) == 0x0200; - conflict = (flags & 0x0400) == 0x0400; - opcode = (flags & 0x7800) >> 11; - query_response = (flags & 0x8000) == 0x8000 - - var cache : Dictionary = {} - - # Fill in the extra properties based on lengths read from packet. - var question_length = packet.get_u16() - var answer_length = packet.get_u16() - var auth_length = packet.get_u16() - var add_length = packet.get_u16() - for i in range(question_length): - dns_questions.append(DNSQuestion.new(packet, cache)) - for i in range(answer_length): - dns_answers.append(DNSRecord.new(packet, cache)) - for i in range(auth_length): - dns_authoritories.append(DNSRecord.new(packet, cache)) - for i in range(add_length): - dns_additional.append(DNSRecord.new(packet, cache)) - -class DNSQuestion: - ## Unsigned Short - Type of question/record. - var dns_type : RECORD_TYPE - ## Unsigned Short - The question/record class. - var dns_class : int - ## Labels for the question - var labels : Array[String] = [] - ## The cache MUST be the same across the entire packet deserialization. - var _cache : Dictionary - ## Initialize the properties. - func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: - _cache = cache - labels = _read_labels(packet) - dns_type = packet.get_u16() - dns_class = packet.get_u16() - - ## Recursively read all labels - func _read_labels(packet : StreamPeerBuffer) -> Array[String]: - var pos = packet.get_position() - var length = packet.get_u8() - # Check if compressed. - if length & 0xC0 == 0xC0: - var pointer = (length ^ 0xC0) << 8 | packet.get_u8() - var cname = _cache[pointer] - _cache[pos] = cname - return cname - var inner_labels : Array[String] = [] - if length == 0: - return inner_labels - - # Get data returns a record of the attempt, and the results from the attempt. - var raw_data = packet.get_data(length)[1] - var packed_data = PackedByteArray(raw_data) - - inner_labels.append(packed_data.get_string_from_utf8()) - inner_labels.append_array(_read_labels(packet)) - _cache[pos] = inner_labels - - return inner_labels - -enum RECORD_TYPE { - A = 1, - NS = 2, - PTR = 12, - TXT = 16, - SRV = 33 -} - -class DNSRecord: - extends DNSQuestion - - ## Time-to-live in seconds - var ttl_seconds : int - ## Length of data - var length : int - ## Structured data (changes depending on record_type) - var data : Dictionary - - ## Read from the packet to initialize the DNS Record - func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: - # Make sure we init the details of the packet. - super(packet, cache) - - ttl_seconds = packet.get_u32() - length = packet.get_u16() - - if dns_type == RECORD_TYPE.A: - _a_record(packet) - elif dns_type == RECORD_TYPE.PTR: - _ptr_record(packet) - elif dns_type == RECORD_TYPE.SRV: - _srv_record(packet) - elif dns_type == RECORD_TYPE.NS: - _ns_record(packet) - elif dns_type == RECORD_TYPE.TXT: - _txt_record(packet) - else: - print("Unsupported DNS record type found: %s", dns_type) - - func _a_record(packet : StreamPeerBuffer) -> void: - data["address"] = _get_ipv4_address(packet) - func _ptr_record(packet : StreamPeerBuffer) -> void: - data["domain_labels"] = _read_labels(packet) - func _srv_record(packet : StreamPeerBuffer) -> void: - data["priority"] = packet.get_u16() - data["weight"] = packet.get_u16() - data["port"] = packet.get_u16() - data["target"] = _read_labels(packet) - func _ns_record(packet : StreamPeerBuffer) -> void: - data["authority"] = _read_labels(packet) - func _txt_record(packet : StreamPeerBuffer) -> void: - data["text"] = "" - var l = length - while l > 0: - var part_length : int = packet.get_u8() - var part : PackedByteArray = PackedByteArray(packet.get_data(part_length)[1]) - var str_part : String = part.get_string_from_ascii() - data["text"] += str_part - l -= part_length + 1 # We +1 here for the part length byte we read. - func _get_ipv4_address(packet : StreamPeerBuffer) -> String: - # WHY - var ip = packet.get_u32() - var ip_bytes : Array[int] = [0, 0, 0, 0] - ip_bytes[0] = int((ip >> 24) & 0xFF) - ip_bytes[1] = int((ip >> 16) & 0xFF) - ip_bytes[2] = int((ip >> 8) & 0XFF) - ip_bytes[3] = int(ip & 0xFF) - - return "%d.%d.%d.%d" % [ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]] - var server : UDPServer var clients : Array[PacketPeerUDP] = [] var multicast_address : String = "224.0.0.251" @@ -173,9 +12,7 @@ func _ready() -> void: server = UDPServer.new() # We only listen on ipv4, we're not using mDNS for IPv6. var err = server.listen(5353, "0.0.0.0") - - if err > 0: - pass + func _process(delta : float) -> void: server.poll() # Important! if server.is_connection_available(): From 9151c54edbd67ac50cc5d08e304879f3d5fbc03b Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 17 May 2025 14:43:32 +0930 Subject: [PATCH 04/23] Separate out to enable send support later --- .../godot-multicast-dns/DNSPacket.gd | 37 ++++++++++------- .../godot-multicast-dns/DNSQuestion.gd | 24 ++++++++--- .../godot-multicast-dns/DNSRecord.gd | 40 +++++++++++-------- .../godot-multicast-dns/MulticastDNS.gd | 2 +- 4 files changed, 65 insertions(+), 38 deletions(-) diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd index abfd5d6..6b93107 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd @@ -24,29 +24,36 @@ var dns_answers : Array[DNSRecord] = [] var dns_authoritories : Array[DNSRecord] = [] ## DNS Additional var dns_additional : Array[DNSRecord] = [] - -func _init(packet : StreamPeerBuffer) -> void: - id = packet.get_u16() + +static func from_packet(packet : StreamPeerBuffer) -> DNSPacket: + var dns_packet : DNSPacket = DNSPacket.new() + + dns_packet.id = packet.get_u16() var flags = packet.get_u16() - response_code = (flags & 0x000F); - tentative = (flags & 0x0100) == 0x0100; - truncation = (flags & 0x0200) == 0x0200; - conflict = (flags & 0x0400) == 0x0400; - opcode = (flags & 0x7800) >> 11; - query_response = (flags & 0x8000) == 0x8000 - + dns_packet.response_code = (flags & 0x000F); + dns_packet.tentative = (flags & 0x0100) == 0x0100; + dns_packet.truncation = (flags & 0x0200) == 0x0200; + dns_packet.conflict = (flags & 0x0400) == 0x0400; + dns_packet.opcode = (flags & 0x7800) >> 11; + dns_packet.query_response = (flags & 0x8000) == 0x8000 + var cache : Dictionary = {} - + # Fill in the extra properties based on lengths read from packet. var question_length = packet.get_u16() var answer_length = packet.get_u16() var auth_length = packet.get_u16() var add_length = packet.get_u16() for i in range(question_length): - dns_questions.append(DNSQuestion.new(packet, cache)) + dns_packet.dns_questions.append(DNSQuestion.from_packet(packet, cache)) for i in range(answer_length): - dns_answers.append(DNSRecord.new(packet, cache)) + dns_packet.dns_answers.append(DNSRecord.from_packet(packet, cache)) for i in range(auth_length): - dns_authoritories.append(DNSRecord.new(packet, cache)) + dns_packet.dns_authoritories.append(DNSRecord.from_packet(packet, cache)) for i in range(add_length): - dns_additional.append(DNSRecord.new(packet, cache)) + dns_packet.dns_additional.append(DNSRecord.from_packet(packet, cache)) + + return dns_packet + +# TODO: Add send support. +# static func to_packet() .... diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd index 63bf11d..1408fdb 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd @@ -8,12 +8,24 @@ var dns_class : int var labels : Array[String] = [] ## The cache MUST be the same across the entire packet deserialization. var _cache : Dictionary -## Initialize the properties. -func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: - _cache = cache - labels = _read_labels(packet) - dns_type = packet.get_u16() - dns_class = packet.get_u16() + +## Extract a DNS Question from the provided packet with the label cache. +static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSQuestion: + var dns_question : DNSQuestion = DNSQuestion.new() + + dns_question._cache = cache + dns_question.labels = dns_question._read_labels(packet) + dns_question.dns_type = packet.get_u16() + dns_question.dns_class = packet.get_u16() + + return dns_question + +## Extract a DNS Question from the provided packet with the label cache, applying to the record. +static func from_packet_for_record(packet : StreamPeerBuffer, cache: Dictionary, record : DNSRecord): + record._cache = cache + record.labels = record._read_labels(packet) + record.dns_type = packet.get_u16() + record.dns_class = packet.get_u16() ## Recursively read all labels func _read_labels(packet : StreamPeerBuffer) -> Array[String]: diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index 7e379e0..b97806e 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -16,38 +16,45 @@ var length : int ## Structured data (changes depending on record_type) var data : Dictionary -## Read from the packet to initialize the DNS Record -func _init(packet : StreamPeerBuffer, cache: Dictionary) -> void: +## Extract a DNS Record from the provided packet with the label cache. +static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSRecord: # Make sure we init the details of the packet. - super(packet, cache) + var dns_record : DNSRecord = DNSRecord.new() + DNSQuestion.from_packet_for_record(packet, cache, dns_record) - ttl_seconds = packet.get_u32() - length = packet.get_u16() + dns_record.ttl_seconds = packet.get_u32() + dns_record.length = packet.get_u16() - if dns_type == RECORD_TYPE.A: - _a_record(packet) - elif dns_type == RECORD_TYPE.PTR: - _ptr_record(packet) - elif dns_type == RECORD_TYPE.SRV: - _srv_record(packet) - elif dns_type == RECORD_TYPE.NS: - _ns_record(packet) - elif dns_type == RECORD_TYPE.TXT: - _txt_record(packet) + if dns_record.dns_type == RECORD_TYPE.A: + dns_record._a_record(packet) + elif dns_record.dns_type == RECORD_TYPE.PTR: + dns_record._ptr_record(packet) + elif dns_record.dns_type == RECORD_TYPE.SRV: + dns_record._srv_record(packet) + elif dns_record.dns_type == RECORD_TYPE.NS: + dns_record._ns_record(packet) + elif dns_record.dns_type == RECORD_TYPE.TXT: + dns_record._txt_record(packet) else: - print("Unsupported DNS record type found: %s", dns_type) + print("Unsupported DNS record type found: %s", dns_record.dns_type) + + return dns_record func _a_record(packet : StreamPeerBuffer) -> void: data["address"] = _get_ipv4_address(packet) + func _ptr_record(packet : StreamPeerBuffer) -> void: data["domain_labels"] = _read_labels(packet) + func _srv_record(packet : StreamPeerBuffer) -> void: data["priority"] = packet.get_u16() data["weight"] = packet.get_u16() data["port"] = packet.get_u16() data["target"] = _read_labels(packet) + func _ns_record(packet : StreamPeerBuffer) -> void: data["authority"] = _read_labels(packet) + func _txt_record(packet : StreamPeerBuffer) -> void: data["text"] = "" var l = length @@ -57,6 +64,7 @@ func _txt_record(packet : StreamPeerBuffer) -> void: var str_part : String = part.get_string_from_ascii() data["text"] += str_part l -= part_length + 1 # We +1 here for the part length byte we read. + func _get_ipv4_address(packet : StreamPeerBuffer) -> String: # WHY var ip = packet.get_u32() diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index 98a80ae..c59f9fd 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -45,6 +45,6 @@ func _process(delta : float) -> void: packet.data_array = packet_bytes packet.big_endian = true - var dns_packet : DNSPacket = DNSPacket.new(packet) + var dns_packet : DNSPacket = DNSPacket.from_packet(packet) on_receive.emit(dns_packet, packet) From 322b9ca860c60587b07ee7a46140f505aee7af2b Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 17 May 2025 15:31:37 +0930 Subject: [PATCH 05/23] Cleanup Whitespace --- Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index c59f9fd..2d7eb21 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -12,6 +12,8 @@ func _ready() -> void: server = UDPServer.new() # We only listen on ipv4, we're not using mDNS for IPv6. var err = server.listen(5353, "0.0.0.0") + if err != OK: + printerr("[Multicast DNS] Failed to start listening on port 5353 with error code %d" % err) func _process(delta : float) -> void: server.poll() # Important! @@ -29,22 +31,22 @@ func _process(delta : float) -> void: for receiver in clients: if receiver.get_available_packet_count() <= 0: continue - + var packet_bytes : PackedByteArray = receiver.get_packet() - + # Make sure it is local, this may be disregarded in some situations in the future? # FIXME: If issues happen, remove this check. var packet_ip = receiver.get_packet_ip() if not local_addresses.has(receiver.get_packet_ip()): continue - + # Packet is big endian. Little endian is all that the extension methods of PackedByteArray support. # We must use a StreamPeerBuffer. # Source: https://github.com/godotengine/godot-proposals/issues/9586#issuecomment-2074227585 var packet : StreamPeerBuffer = StreamPeerBuffer.new() packet.data_array = packet_bytes packet.big_endian = true - + var dns_packet : DNSPacket = DNSPacket.from_packet(packet) - + on_receive.emit(dns_packet, packet) From 664c92d22b7807c1b20206e36110c71dfd33f421 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 17 May 2025 23:42:53 +0930 Subject: [PATCH 06/23] Add basic VRChat Face Tracking Sending - Support v5.0.0 only - Limited implemented mapping from mediapipe to unified shapes. - Eyes and other individually implemented shapes are not implemented currently. --- Mods/VRChatOSC/VRCParam.gd | 62 ++++++ Mods/VRChatOSC/VRCParam.gd.uid | 1 + Mods/VRChatOSC/VRCParams.gd | 118 +++++++++++ Mods/VRChatOSC/VRCParams.gd.uid | 1 + Mods/VRChatOSC/VRChatOSC.gd | 197 +++++++++++++++++- Mods/VRChatOSC/VRChatOSC.tscn | 10 +- .../godot-multicast-dns/DNSQuestion.gd | 4 + .../godot-multicast-dns/DNSRecord.gd | 1 + 8 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 Mods/VRChatOSC/VRCParam.gd create mode 100644 Mods/VRChatOSC/VRCParam.gd.uid create mode 100644 Mods/VRChatOSC/VRCParams.gd create mode 100644 Mods/VRChatOSC/VRCParams.gd.uid diff --git a/Mods/VRChatOSC/VRCParam.gd b/Mods/VRChatOSC/VRCParam.gd new file mode 100644 index 0000000..0207a39 --- /dev/null +++ b/Mods/VRChatOSC/VRCParam.gd @@ -0,0 +1,62 @@ +extends Node +class_name VRCParam + +var key : String +var type : String +var value : Variant +var full_path : String +var avatar_id : String +var param_binary : bool = false +var param_float : bool = false +var binary_key : String +var binary_exponent : int +var is_dirty : bool = false + +func _init(param_path : String, pkey : String, ptype : String, pavatar_id : String, pvalue) -> void: + full_path = param_path + key = pkey + type = ptype + value = pvalue + avatar_id = pavatar_id + param_binary = is_binary() + param_float = is_float() + if param_binary: + if key.ends_with("Negative"): + binary_key = key.replace("Negative", "") + # Negative exponent is "0". + binary_exponent = 0 + else: + if key.ends_with("16") or key.ends_with("32") or key.ends_with("64"): + binary_exponent = int(key.substr(len(key) - 2)) + binary_key = key.substr(0, len(key) - 2) + else: + binary_exponent = int(key.substr(len(key) - 1)) + binary_key = key.substr(0, len(key) - 1) + else: + # Helps with searching. + binary_key = key + +func is_binary() -> bool: + var key_name = key.ends_with("Negative") \ + or key.ends_with("1") \ + or key.ends_with("2") \ + or key.ends_with("4") \ + or key.ends_with("8") \ + or key.ends_with("16") \ + or key.ends_with("32") \ + or key.ends_with("64") + var val_type = type == "T" + return key_name and val_type + +func is_float() -> bool: + return type == "f" + +func update_value(new_value : Variant) -> void: + value = new_value + is_dirty = true + +func reset_dirty() -> void: + is_dirty = false + +func to_osc() -> PackedByteArray: + return PackedByteArray() diff --git a/Mods/VRChatOSC/VRCParam.gd.uid b/Mods/VRChatOSC/VRCParam.gd.uid new file mode 100644 index 0000000..9092f63 --- /dev/null +++ b/Mods/VRChatOSC/VRCParam.gd.uid @@ -0,0 +1 @@ +uid://dg23esm6tfg5f diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd new file mode 100644 index 0000000..22cf9df --- /dev/null +++ b/Mods/VRChatOSC/VRCParams.gd @@ -0,0 +1,118 @@ +extends Node +class_name VRCParams + +var _params : Array[VRCParam] = [] +var _has_changed_avi : bool = false +var _avatar_id : String +var _raw_params : Dictionary +var _binary_params : Dictionary +var _float_params : Dictionary + +func reset(): + _params = [] + _has_changed_avi = false + _avatar_id = "" + _raw_params = {} + +func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_avi : bool): + _raw_params = raw_avatar_params + _avatar_id = avatar_id + _has_changed_avi = has_changed_avi + + if raw_avatar_params.has("v2"): + raw_avatar_params = raw_avatar_params["v2"]["CONTENTS"] + _raw_params = raw_avatar_params + + # Only if we are wanting to update/change param values do we progress here. + var param_names = raw_avatar_params.keys() + for key in param_names: + # Verify this is a type/value parameter. + if not "TYPE" in raw_avatar_params[key] or not "VALUE" in raw_avatar_params[key]: + continue + # FIXME: Value can actually be >0, or == 0. + var param = VRCParam.new( + raw_avatar_params[key]["FULL_PATH"], + key, + raw_avatar_params[key]["TYPE"], + avatar_id, + raw_avatar_params[key]["VALUE"][0] + ) + if param.param_binary: + if not _binary_params.has(param.binary_key): + _binary_params[param.binary_key] = [] + _binary_params[param.binary_key].append(param) + elif param.param_float: + assert(not _float_params.has(param.key), "Already existing float parameter with key %s" % param.key) + _float_params[param.key] = param + _params.append( + param + ) + pass + +## Updates a particular key to the supplied value. +## This func takes care of the exchange between binary/float parameters in VRC tracking. +func update_value(key : String, value): + var params : Array[VRCParam] = _params.filter(func (p : VRCParam): return p.binary_key == key) + if len(params) == 0: + return + + var param : VRCParam = params[0] + + if param.is_binary(): + # This is actually an Array[VRCParam] but ... Godot... + var param_group : Array = _binary_params[param.binary_key] + + # Convert key to binary. + var is_neg = value < 0.0 + + # Important to normalize to positive. + var val_pos = absf(value) + + # Make sure we take care of neg. + var neg_params : Array = param_group.filter(func (p : VRCParam): return p.binary_exponent == 0) + if len(neg_params) == 1: + neg_params[0].update_value(is_neg) + + + param_group.sort_custom( + func (a : VRCParam, b : VRCParam): + return a.binary_exponent < b.binary_exponent + ) + + # 1. Determine N (number of magnitude bits) + var N : int = len(param_group.filter(func (p : VRCParam): return p.binary_exponent != 0)) + + # 2. Convert val_pos (0.0-1.0 float) to an integer representation (0 to 2^N - 1) + var integer_representation : int + if N == 0: + integer_representation = 0 + else: + # Scale val_pos to the range [0, 2^N]. Example N=3, range [0,8]. + var scaled_value : float = val_pos * pow(2.0, float(N)) + # Take the floor to get the discrete step. + integer_representation = floori(scaled_value) + # Clamp the integer_representation to be within [0, 2^N - 1]. + # (e.g. if N=3, max_val is 7). + var max_representable_int_val : int = int(pow(2.0, float(N))) - 1 + integer_representation = mini(integer_representation, max_representable_int_val) + integer_representation = maxi(integer_representation, 0) + + # 3. Set bits for each magnitude parameter + var num = 0 + for exp_param : VRCParam in param_group: + if exp_param.binary_exponent == 0: + continue + var bit : int = integer_representation & (1 << num) + exp_param.update_value(not bit == 0) + num += 1 + pass + elif param.is_float(): + param.update_value(value) + +## Get all parameters that have had values change since last use. +## Caller should reset is_dirty flag on the parameter after sending. +func get_dirty() -> Array[VRCParam]: + return _params.filter(func (p : VRCParam): return p.is_dirty) + +func get_all() -> Array[VRCParam]: + return _params diff --git a/Mods/VRChatOSC/VRCParams.gd.uid b/Mods/VRChatOSC/VRCParams.gd.uid new file mode 100644 index 0000000..d0fe21c --- /dev/null +++ b/Mods/VRChatOSC/VRCParams.gd.uid @@ -0,0 +1 @@ +uid://by0fxubrjwwlq diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index 0c167ee..decfb9a 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -2,13 +2,204 @@ extends Mod_Base class_name VRChatOSC @export var dns_service : MulticastDNS +@export var update_vrc_param_values : bool = false +@export var osc_client : KiriOSClient + +var vrchat_osc_query_endpoint : String = "" +## The current value of the avatar ID. +var current_avatar_id : String +## The previous value of the avatar ID. +var previous_avatar_id : String +## Dictionary of avatar parameters and their current values. +var raw_avatar_params : Dictionary +## Parsed VRChat parameters. +var vrc_params : VRCParams = VRCParams.new() +## Keys for quick lookup and verification. +var vrc_param_keys : Array[String] = [] +var avatar_req : HTTPRequest +var client_send_rate_limit_ms : int = 500 +var curr_client_send_time : float +# Can we not JUST USE THE SAME MAPPING +# WHY DOES EVERY APP NEED THEIR OWN WAY +var unified_to_arkit_mapping : Dictionary = { + "EyeLookUpRight": "eyeLookUpRight", + "EyeLookDownRight": "eyeLookDownRight", + "EyeLookInRight": "eyeLookInRight", + "EyeLookOutRight": "eyeLookOutRight", + "EyeLookUpLeft": "eyeLookUpLeft", + "EyeLookDownLeft": "eyeLookDownLeft", + "EyeLookInLeft": "eyeLookInLeft", + "EyeLookOutLeft": "eyeLookOutLeft", + "EyeClosedRight": "eyeBlinkRight", + "EyeClosedLeft": "eyeBlinkLeft", + "EyeSquintRight": "eyeSquintRight", + "EyeSquintLeft": "eyeSquintLeft", + "EyeWideRight": "eyeWideRight", + "EyeWideLeft": "eyeWideLeft", + "BrowDownRight": "browDownRight", + "BrowDownLeft": "browDownLeft", + "BrowInnerUp": "browInnerUp", + "BrowOuterUpRight": "browOuterUpRight", + "BrowOuterUpLeft": "browOuterUpLeft", + "NoseSneerRight": "noseSneerRight", + "NoseSneerLeft": "noseSneerLeft", + "CheekSquintRight": "cheekSquintRight", + "CheekSquintLeft": "cheekSquintLeft", + "CheekPuff": "cheekPuff", + "JawOpen": "jawOpen", + "MouthClosed": "mouthClose", + "JawRight": "jawRight", + "JawLeft": "jawLeft", + "JawForward": "jawForward", + "LipSuckUpper": "mouthRollUpper", + "LipSuckLower": "mouthRollLower", + "LipFunnel": "mouthFunnel", + "LipPucker": "mouthPucker", + "MouthUpperUpRight": "mouthUpperUpRight", + "MouthUpperUpLeft": "mouthUpperUpLeft", + "MouthLowerDownRight": "mouthLowerUpRight", + "MouthLowerDownLeft": "mouthLowerUpLeft", + "MouthSmileRight": "mouthSmileRight", + "MouthSmileLeft": "mouthSmileLeft", + "MouthFrownRight": "mouthFrownRight", + "MouthFrownLeft": "mouthFrownLeft", + "MouthStretchRight": "mouthStretchRight", + "MouthStretchLeft": "mouthStretchLeft", + "MouthDimplerRight": "mouthDimpleRight", + "MouthDimplerLeft": "mouthDimpleLeft", + "MouthRaiserUpper": "mouthShrugUpper", + "MouthRaiserLower": "mouthShrugLower", + "MouthPressRight": "mouthPressRight", + "MouthPressLeft": "mouthPressLeft", + "TongueOut": "tongueOut" +} +var arkit_to_unified_mapping : Dictionary = {} func _ready() -> void: + avatar_req = HTTPRequest.new() + add_child(avatar_req) + avatar_req.request_completed.connect(_avatar_params_request_complete) dns_service.on_receive.connect(_dns_packet) + for key in unified_to_arkit_mapping: + var new_key = unified_to_arkit_mapping[key] + var new_value = key + arkit_to_unified_mapping[new_key] = new_value + +var get_a = true + +func _process(delta : float) -> void: + + if vrchat_osc_query_endpoint == "": + return + + curr_client_send_time += delta + if curr_client_send_time > client_send_rate_limit_ms / 1000: + curr_client_send_time = 0 + var blendshapes : Dictionary = get_global_mod_data("BlendShapes") + + var unified_blendshapes : Dictionary = {} + for blendshape in blendshapes: + if not arkit_to_unified_mapping.has(blendshape): + continue + var unified_blendshape = arkit_to_unified_mapping[blendshape] + unified_blendshapes[unified_blendshape] = blendshapes[blendshape] + + for shape in unified_blendshapes: + vrc_params.update_value(shape, unified_blendshapes[shape]) + + var to_send_osc : Array[VRCParam] = vrc_params.get_dirty() + #print(len(to_send_osc)) -func _process(_delta : float) -> void: - pass + for param in to_send_osc: + param.reset_dirty() + # We send the message with the full path for the avatar parameter, and type. + var type = param.type + if param.type == "T": + if param.value == true: + type = "T" + else: + type = "F" + osc_client.send_osc_message(param.full_path, type, [param.value]) + + await get_tree().create_timer(10).timeout + if not get_a: + return + get_a = false + _get_avatar_params() func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: - pass + if not packet.query_response: + return + if len(packet.dns_answers) == 0 or len(packet.dns_additional) == 0: + return + + var ptr_record : DNSRecord = packet.dns_answers[0] + if ptr_record.dns_type != DNSRecord.RECORD_TYPE.PTR: + return + + if ptr_record.full_label != "_oscjson._tcp.local": + return + + var domain_label : String = ptr_record.data["full_label"] + if not domain_label.begins_with("VRChat-Client"): + return + + var a_records : Array[DNSRecord] = packet.dns_additional.filter( + func (x : DNSRecord) -> bool: return x.dns_type == DNSRecord.RECORD_TYPE.A + ) + if len(a_records) == 0: + return + var srv_records : Array[DNSRecord] = packet.dns_additional.filter( + func (x : DNSRecord) -> bool: return x.dns_type == DNSRecord.RECORD_TYPE.SRV + ) + if len(srv_records) == 0: + return + var ip_address : String = a_records[0].data["address"] + var port : int = srv_records[0].data["port"] + vrchat_osc_query_endpoint = "http://%s:%s" % \ + [ + ip_address, + port + ] + + if not osc_client.is_client_active(): + # Init osc sender. Default to 9000 (default OSC port). + osc_client.change_port_and_ip(9000, ip_address) + osc_client.start_client() + + print("[VRChat OSC] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) + +func _get_avatar_params(): + if vrchat_osc_query_endpoint == "": + return null + var err = avatar_req.request(vrchat_osc_query_endpoint + "/avatar") + if err != OK: + printerr("[VRChat OSC] Failed to request VRC avatar parameters with error code: %d" % err) + +func _avatar_params_request_complete(result : int, response_code : int, + headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + printerr("Request for VRC avatar params failed.") + return + var json = JSON.parse_string(body.get_string_from_utf8()) + # Uh oh... that's a lot of hardcoded values. + # FIXME: Check that all these keys exist. + current_avatar_id = json["CONTENTS"]["change"]["VALUE"][0] + var has_changed_avi : bool = current_avatar_id != previous_avatar_id + if has_changed_avi: + # Update only if changed avi. + print("[VRChat OSC] Avatar has changed. Updating parameter keys, values and types.") + vrc_param_keys = [] + vrc_params.reset() + + # We always pull raw avatar params to update the current value. + raw_avatar_params = json["CONTENTS"]["parameters"]["CONTENTS"] + + if not update_vrc_param_values and not has_changed_avi: + previous_avatar_id = current_avatar_id + return + + vrc_params.initialize(raw_avatar_params, current_avatar_id, has_changed_avi) + + previous_avatar_id = current_avatar_id diff --git a/Mods/VRChatOSC/VRChatOSC.tscn b/Mods/VRChatOSC/VRChatOSC.tscn index 0353d81..6ff32be 100644 --- a/Mods/VRChatOSC/VRChatOSC.tscn +++ b/Mods/VRChatOSC/VRChatOSC.tscn @@ -1,13 +1,19 @@ -[gd_scene load_steps=3 format=3 uid="uid://cpe3ulnjnrapo"] +[gd_scene load_steps=4 format=3 uid="uid://cpe3ulnjnrapo"] [ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatOSC.gd" id="1_x840n"] [ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_b4fua"] +[ext_resource type="Script" uid="uid://goefhxca5k8g" path="res://Mods/VMCController/KiriOSC/KiriOSCClient.gd" id="3_faft7"] -[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service")] +[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service", "osc_client")] script = ExtResource("1_x840n") dns_service = NodePath("MulticastDNS") +osc_client = NodePath("KiriOSClient") metadata/_custom_type_script = "uid://bsjotf2wtnmeq" [node name="MulticastDNS" type="Node" parent="."] script = ExtResource("2_b4fua") metadata/_custom_type_script = "uid://bfemeu7ysbxwc" + +[node name="KiriOSClient" type="Node" parent="."] +script = ExtResource("3_faft7") +metadata/_custom_type_script = "uid://goefhxca5k8g" diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd index 1408fdb..4ddd25f 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd @@ -6,6 +6,8 @@ var dns_type : int var dns_class : int ## Labels for the question var labels : Array[String] = [] +## The full label is just the labels joined. +var full_label : String ## The cache MUST be the same across the entire packet deserialization. var _cache : Dictionary @@ -15,6 +17,7 @@ static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSQues dns_question._cache = cache dns_question.labels = dns_question._read_labels(packet) + dns_question.full_label = ".".join(dns_question.labels) dns_question.dns_type = packet.get_u16() dns_question.dns_class = packet.get_u16() @@ -24,6 +27,7 @@ static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSQues static func from_packet_for_record(packet : StreamPeerBuffer, cache: Dictionary, record : DNSRecord): record._cache = cache record.labels = record._read_labels(packet) + record.full_label = ".".join(record.labels) record.dns_type = packet.get_u16() record.dns_class = packet.get_u16() diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index b97806e..29ba659 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -45,6 +45,7 @@ func _a_record(packet : StreamPeerBuffer) -> void: func _ptr_record(packet : StreamPeerBuffer) -> void: data["domain_labels"] = _read_labels(packet) + data["full_label"] = ".".join(data["domain_labels"]) func _srv_record(packet : StreamPeerBuffer) -> void: data["priority"] = packet.get_u16() From b0ff21e6e28fa0225be6e5f253c9ea1a5aba5f33 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sun, 25 May 2025 20:29:54 +0930 Subject: [PATCH 07/23] Add basic transform rules Need to tidy up and fix issues with eyes. Need to also add more rules! --- Mods/VRChatOSC/VRCParams.gd | 8 +- Mods/VRChatOSC/VRChatOSC.gd | 353 ++++++++++++++++++++++++++++++++---- 2 files changed, 328 insertions(+), 33 deletions(-) diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index 22cf9df..87c14f4 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -18,7 +18,7 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ _raw_params = raw_avatar_params _avatar_id = avatar_id _has_changed_avi = has_changed_avi - + if raw_avatar_params.has("v2"): raw_avatar_params = raw_avatar_params["v2"]["CONTENTS"] _raw_params = raw_avatar_params @@ -29,7 +29,7 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ # Verify this is a type/value parameter. if not "TYPE" in raw_avatar_params[key] or not "VALUE" in raw_avatar_params[key]: continue - # FIXME: Value can actually be >0, or == 0. + # FIXME: Len of Value can actually be >0, or == 0. var param = VRCParam.new( raw_avatar_params[key]["FULL_PATH"], key, @@ -116,3 +116,7 @@ func get_dirty() -> Array[VRCParam]: func get_all() -> Array[VRCParam]: return _params + +func has(shape_key : String) -> bool: + var params : Array[VRCParam] = _params.filter(func (p : VRCParam): return p.binary_key == shape_key) + return len(params) > 0 diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index decfb9a..66ba949 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -10,8 +10,6 @@ var vrchat_osc_query_endpoint : String = "" var current_avatar_id : String ## The previous value of the avatar ID. var previous_avatar_id : String -## Dictionary of avatar parameters and their current values. -var raw_avatar_params : Dictionary ## Parsed VRChat parameters. var vrc_params : VRCParams = VRCParams.new() ## Keys for quick lookup and verification. @@ -19,6 +17,7 @@ var vrc_param_keys : Array[String] = [] var avatar_req : HTTPRequest var client_send_rate_limit_ms : int = 500 var curr_client_send_time : float +var processing_request : bool = false # Can we not JUST USE THE SAME MAPPING # WHY DOES EVERY APP NEED THEIR OWN WAY var unified_to_arkit_mapping : Dictionary = { @@ -74,6 +73,269 @@ var unified_to_arkit_mapping : Dictionary = { "TongueOut": "tongueOut" } var arkit_to_unified_mapping : Dictionary = {} +enum COMBINATION_TYPE { + RANGE = 1, + COPY = 2, + AVERAGE = 3 +} +enum SHAPE_KEY_TYPE { + MEDIAPIPE = 1, + UNIFIED = 2 +} +enum DIRECTION { + POSITIVE = 1, + NEGATIVE = 2 +} +var simplified_parameter_mapping : Dictionary = { + "JawX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawZ": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawForward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawBackward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLidRight": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLidLeft": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLid": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeRightX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookOutRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookInRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeRightY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookInLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookOutLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeX": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, + "EyeY": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, +} + +func _apply_transform_rules_a(unified_blendshapes : Dictionary): + # Take in the simplified_parameter_mapping dictionary, + # For each rule in this, identify the rule type then apply the action of the combination type + # Store the resulting value or changes in the unified blendshapes parameter + + # For example: + # EyeLidLeft is a copy of EyeClosedLeft, so it processes the combination type of copy + # Once identified as copy operation, it takes the first combination_shapes item and copies it to last shape + # This copy operation MUST take into consideration what the shape type is. + # You can use the arkit_to_unified_mapping dictionary to map arkit (mediapipe) to unified. + # This dictionary has the key as arkit (mediapipe) and the value as the associated unified shape key. + # You can use the unified_to_arkit_mapping dictionary to map a unified to arkit. + # Current values are always stored in the passed in parameter (unified format) + + # Another example: + # JawX is defined as a value (float) that uses JawRight and JawLeft to form the value + # <0.0 -> 1.0> Jaw Right + # <0.0 -> -1.0> Jaw Left + # Notice the value shifts in direction from positive to negative 1. + # This direction is defined in the particular combination shape, as "direction". + # The final JawX value will reflect the current values of JawRight/JawLeft + # These values must be resolved in the parameter provided to this func. + # All inputs are stored as unified, but shape keys might need to be converted from MediaPipe to Unified. + # See previous example for how to do this. + return {} + +func _get_unified_value(shape : String, shape_type : SHAPE_KEY_TYPE, unified_blendshapes : Dictionary) -> float: + if shape_type == SHAPE_KEY_TYPE.UNIFIED: + return unified_blendshapes.get(shape, 0.0) + elif shape_type == SHAPE_KEY_TYPE.MEDIAPIPE: + var unified_shape: String = arkit_to_unified_mapping.get(shape, shape) + return unified_blendshapes.get(unified_shape, 0.0) + return 0.0 + +func _get_unified_shape(shape: String, shape_type: SHAPE_KEY_TYPE) -> String: + if shape_type == SHAPE_KEY_TYPE.UNIFIED: + return shape + elif shape_type == SHAPE_KEY_TYPE.MEDIAPIPE: + return arkit_to_unified_mapping.get(shape, shape) + return shape + +func _apply_transform_rules(unified_blendshapes : Dictionary) -> void: + for param_name : String in simplified_parameter_mapping.keys(): + var rule : Dictionary = simplified_parameter_mapping[param_name] + var comb_type : int = rule["combination_type"] + var shapes : Array = rule["combination_shapes"] + + match comb_type: + COMBINATION_TYPE.COPY: + var src_shape_info : Dictionary = shapes[0] + var src_shape : String = src_shape_info["shape"] + var src_type : SHAPE_KEY_TYPE = src_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var src_value : float = _get_unified_value(src_shape, src_type, unified_blendshapes) + var src_inverse : bool = src_shape_info.get("inverse", false) + if src_inverse: + if src_value < 0: + src_value = abs(src_value) + else: + src_value *= -1 + + for i in range(1, shapes.size()): + var dst_shape_info : Dictionary = shapes[i] + var dst_shape : String = dst_shape_info["shape"] + var dst_type : SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var unified_shape : String = _get_unified_shape(dst_shape, dst_type) + unified_blendshapes[unified_shape] = src_value + + COMBINATION_TYPE.AVERAGE: + var sum : float = 0.0 + var count : int = 0 + for shape_info : Dictionary in shapes: + var shape : String = shape_info["shape"] + var shape_type : SHAPE_KEY_TYPE = shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + sum += value + count += 1 + unified_blendshapes[param_name] = sum / max(count, 1) + + COMBINATION_TYPE.RANGE: + var total : float = 0.0 + for shape_info : Dictionary in shapes: + var shape : String = shape_info["shape"] + var shape_type : SHAPE_KEY_TYPE = shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var direction : DIRECTION = shape_info.get("direction", DIRECTION.POSITIVE) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + if direction == DIRECTION.POSITIVE: + total += value + else: + total -= value + unified_blendshapes[param_name] = total func _ready() -> void: avatar_req = HTTPRequest.new() @@ -95,37 +357,59 @@ func _process(delta : float) -> void: curr_client_send_time += delta if curr_client_send_time > client_send_rate_limit_ms / 1000: curr_client_send_time = 0 - var blendshapes : Dictionary = get_global_mod_data("BlendShapes") + + # Map the blendshapes we have from mediapipe to the unified versions. + var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() + + _apply_transform_rules(unified_blendshapes) - var unified_blendshapes : Dictionary = {} - for blendshape in blendshapes: - if not arkit_to_unified_mapping.has(blendshape): - continue - var unified_blendshape = arkit_to_unified_mapping[blendshape] - unified_blendshapes[unified_blendshape] = blendshapes[blendshape] + # Handle mapping simplified parameters + # TODO: Eye parameters + + # TODO: Brow parameters + + # TODO: Mouth parameters + + # TODO: Lip parameters + + # TODO: Nose and cheek parameters + + + # Set params to values for shape in unified_blendshapes: vrc_params.update_value(shape, unified_blendshapes[shape]) - var to_send_osc : Array[VRCParam] = vrc_params.get_dirty() - #print(len(to_send_osc)) - - for param in to_send_osc: - param.reset_dirty() - # We send the message with the full path for the avatar parameter, and type. - var type = param.type - if param.type == "T": - if param.value == true: - type = "T" - else: - type = "F" - osc_client.send_osc_message(param.full_path, type, [param.value]) - - await get_tree().create_timer(10).timeout - if not get_a: - return - get_a = false - _get_avatar_params() + # Finally, send all dirty params off to VRC + _send_dirty_params() + + +func _map_blendshapes_to_unified() -> Dictionary: + var blendshapes : Dictionary = get_global_mod_data("BlendShapes") + var unified_blendshapes : Dictionary = {} + for blendshape in blendshapes: + if not arkit_to_unified_mapping.has(blendshape): + continue + var unified_blendshape = arkit_to_unified_mapping[blendshape] + unified_blendshapes[unified_blendshape] = blendshapes[blendshape] + + return unified_blendshapes + +func _send_dirty_params(): + var to_send_osc : Array[VRCParam] = vrc_params.get_dirty() + + for param in to_send_osc: + param.reset_dirty() + # We send the message with the full path for the avatar parameter, and type. + var type = param.type + if param.type == "T": + # Param value is true? Send as type "T" representing "True" in OSC. + if param.value: + type = "T" + else: + type = "F" + osc_client.send_osc_message(param.full_path, type, [param.value]) + func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: if not packet.query_response: @@ -168,19 +452,24 @@ func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: osc_client.start_client() print("[VRChat OSC] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) + _get_avatar_params() func _get_avatar_params(): if vrchat_osc_query_endpoint == "": - return null + return + if processing_request: + return var err = avatar_req.request(vrchat_osc_query_endpoint + "/avatar") + processing_request = true if err != OK: printerr("[VRChat OSC] Failed to request VRC avatar parameters with error code: %d" % err) func _avatar_params_request_complete(result : int, response_code : int, headers: PackedStringArray, body: PackedByteArray) -> void: - if result != HTTPRequest.RESULT_SUCCESS: + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: printerr("Request for VRC avatar params failed.") return + print("[VRChat OSC] Avatar param request complete.") var json = JSON.parse_string(body.get_string_from_utf8()) # Uh oh... that's a lot of hardcoded values. @@ -194,7 +483,9 @@ func _avatar_params_request_complete(result : int, response_code : int, vrc_params.reset() # We always pull raw avatar params to update the current value. - raw_avatar_params = json["CONTENTS"]["parameters"]["CONTENTS"] + var raw_avatar_params = json["CONTENTS"]["parameters"]["CONTENTS"] + + processing_request = false if not update_vrc_param_values and not has_changed_avi: previous_avatar_id = current_avatar_id From 6dd68bd5a91fae6e5131c39ecc23dbd6592cc6d8 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 26 Jul 2025 19:27:21 +0930 Subject: [PATCH 08/23] Add weighted parameter combining --- Mods/VRChatOSC/VRCParam.gd | 10 ++- Mods/VRChatOSC/VRChatOSC.gd | 125 +++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/Mods/VRChatOSC/VRCParam.gd b/Mods/VRChatOSC/VRCParam.gd index 0207a39..dd0b55f 100644 --- a/Mods/VRChatOSC/VRCParam.gd +++ b/Mods/VRChatOSC/VRCParam.gd @@ -26,7 +26,10 @@ func _init(param_path : String, pkey : String, ptype : String, pavatar_id : Stri # Negative exponent is "0". binary_exponent = 0 else: - if key.ends_with("16") or key.ends_with("32") or key.ends_with("64"): + if key.ends_with("128") or key.ends_with("256") or key.ends_with("512"): + binary_exponent = int(key.substr(len(key) - 3)) + binary_key = key.substr(0, len(key) - 3) + elif key.ends_with("16") or key.ends_with("32") or key.ends_with("64"): binary_exponent = int(key.substr(len(key) - 2)) binary_key = key.substr(0, len(key) - 2) else: @@ -44,7 +47,10 @@ func is_binary() -> bool: or key.ends_with("8") \ or key.ends_with("16") \ or key.ends_with("32") \ - or key.ends_with("64") + or key.ends_with("64") \ + or key.ends_with("128") \ + or key.ends_with("256") \ + or key.ends_with("512") var val_type = type == "T" return key_name and val_type diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index 66ba949..c2f268c 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -76,7 +76,8 @@ var arkit_to_unified_mapping : Dictionary = {} enum COMBINATION_TYPE { RANGE = 1, COPY = 2, - AVERAGE = 3 + AVERAGE = 3, + WEIGHTED = 4 } enum SHAPE_KEY_TYPE { MEDIAPIPE = 1, @@ -87,6 +88,111 @@ enum DIRECTION { NEGATIVE = 2 } var simplified_parameter_mapping : Dictionary = { + "BrowUpRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowUpLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowDownRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "BrowDownLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, "JawX": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -305,7 +411,7 @@ func _apply_transform_rules(unified_blendshapes : Dictionary) -> void: src_value = abs(src_value) else: src_value *= -1 - + for i in range(1, shapes.size()): var dst_shape_info : Dictionary = shapes[i] var dst_shape : String = dst_shape_info["shape"] @@ -337,6 +443,21 @@ func _apply_transform_rules(unified_blendshapes : Dictionary) -> void: total -= value unified_blendshapes[param_name] = total + COMBINATION_TYPE.WEIGHTED: + var src_shape_info : Dictionary = shapes[0] + var src_shape : String = src_shape_info["shape"] + var src_type : SHAPE_KEY_TYPE = src_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var src_value : float = _get_unified_value(src_shape, src_type, unified_blendshapes) + var src_weight : float = src_shape_info["weight"] + + var dst_shape_info : Dictionary = shapes[1] + var dst_shape : String = dst_shape_info["shape"] + var dst_type : SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var dst_value : float = _get_unified_value(dst_shape, dst_type, unified_blendshapes) + var dst_weight : float = dst_shape_info["weight"] + + unified_blendshapes[param_name] = src_value * src_weight + dst_value * dst_weight + func _ready() -> void: avatar_req = HTTPRequest.new() add_child(avatar_req) From 5f642175d5772605e2d7966d4d8fa3567f7d9322 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 26 Jul 2025 20:06:34 +0930 Subject: [PATCH 09/23] Add DNS Query Sending --- .../godot-multicast-dns/DNSPacket.gd | 43 ++++++++++++++- .../godot-multicast-dns/DNSQuestion.gd | 24 +++++++++ .../godot-multicast-dns/DNSRecord.gd | 54 +++++++++++++++++++ .../godot-multicast-dns/MulticastDNS.gd | 6 +++ Mods/VRChatOSC/godot-multicast-dns/README.md | 5 +- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd index 6b93107..1ea71f6 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSPacket.gd @@ -55,5 +55,44 @@ static func from_packet(packet : StreamPeerBuffer) -> DNSPacket: return dns_packet -# TODO: Add send support. -# static func to_packet() .... +## To raw byte packet for sending. +func to_packet() -> StreamPeerBuffer: + var packet := StreamPeerBuffer.new() + packet.big_endian = true + packet.put_u16(id) + + var flags := 0 + flags |= (response_code & 0xF) + if tentative: flags |= 0x0100 + if truncation: flags |= 0x0200 + if conflict: flags |= 0x0400 + flags |= ((opcode & 0xF) << 11) + if query_response: + flags |= 0x8000 + packet.put_u16(flags) + + # qdcount, ancount, nscount, arcount + packet.put_u16(dns_questions.size()) + packet.put_u16(dns_answers.size()) + packet.put_u16(dns_authoritories.size()) + packet.put_u16(dns_additional.size()) + + # Prepare a cache for name compression: domain_name -> packet offset + var cache: Dictionary = {} + + # Serialize questions + for question : DNSQuestion in dns_questions: + question.to_packet(packet, cache) + + # Serialize answers + for answer : DNSRecord in dns_answers: + answer.to_packet(packet, cache) + + for auth : DNSRecord in dns_authoritories: + auth.to_packet(packet, cache) + + for add : DNSRecord in dns_additional: + add.to_packet(packet, cache) + + packet.seek(0) + return packet diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd index 4ddd25f..a2ed645 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSQuestion.gd @@ -31,6 +31,12 @@ static func from_packet_for_record(packet : StreamPeerBuffer, cache: Dictionary, record.dns_type = packet.get_u16() record.dns_class = packet.get_u16() +## Writes the current DNS Question to the packet. +func to_packet(packet: StreamPeerBuffer, cache: Dictionary): + _write_labels(packet, labels, cache) + packet.put_u16(dns_type) + packet.put_u16(dns_class) + ## Recursively read all labels func _read_labels(packet : StreamPeerBuffer) -> Array[String]: var pos = packet.get_position() @@ -54,3 +60,21 @@ func _read_labels(packet : StreamPeerBuffer) -> Array[String]: _cache[pos] = inner_labels return inner_labels + +func _write_labels(packet: StreamPeerBuffer, cur_labels: Array[String], cache: Dictionary) -> void: + var i = 0 + while i < cur_labels.size(): + var suffix = ".".join(cur_labels.slice(i)) + if cache.has(suffix): + var ptr = cache[suffix] + packet.put_u8(0xC0 | (ptr >> 8)) + packet.put_u8(ptr & 0xFF) + return + else: + cache[suffix] = packet.get_position() + var label = cur_labels[i] + var raw = label.to_utf8_buffer() + packet.put_u8(raw.size()) + packet.put_data(raw) + i += 1 + packet.put_u8(0) diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index 29ba659..48330ff 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -40,6 +40,60 @@ static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSReco return dns_record +func to_packet(packet: StreamPeerBuffer, cache: Dictionary) -> void: + super.to_packet(packet, cache) + packet.put_u32(ttl_seconds) + var rdlength_offset = packet.get_position() + packet.put_u16(0) + var rdata_start = packet.get_position() + match dns_type: + RECORD_TYPE.A: + # IPv4: 4 octets + # data["address"] is a string "x.x.x.x" + var parts = data["address"].split(".") + for p in parts: + packet.put_u8(int(p)) + + RECORD_TYPE.PTR: + super._write_labels(packet, data["domain_labels"], cache) + + RECORD_TYPE.NS: + super._write_labels(packet, data["authority"], cache) + + RECORD_TYPE.SRV: + # priority, weight, port, target + packet.put_u16(data["priority"]) + packet.put_u16(data["weight"]) + packet.put_u16(data["port"]) + # target is a domain name (labels array) + var srv_q = DNSQuestion.new() + srv_q.labels = data["target"] + srv_q.dns_type = 0 + srv_q.dns_class = 0 + srv_q.to_packet(packet, cache) + + RECORD_TYPE.TXT: + var txt = data["text"] + var pos = 0 + while pos < txt.length(): + var chunk_size = min(255, txt.length() - pos) + var chunk = txt.substr(pos, chunk_size) + var raw = chunk.to_utf8_buffer() + packet.put_u8(raw.size()) + packet.put_data(raw) + pos += chunk_size + _: + push_error("Unsupported RDATA serialization for type %d" % dns_type) + + var rdata_end = packet.get_position() + var rdlength = rdata_end - rdata_start + + # go back and patch + var cur = packet.get_position() + packet.seek(rdlength_offset) + packet.put_u16(rdlength) + packet.seek(cur) + func _a_record(packet : StreamPeerBuffer) -> void: data["address"] = _get_ipv4_address(packet) diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index 2d7eb21..6fb8fd1 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -50,3 +50,9 @@ func _process(delta : float) -> void: var dns_packet : DNSPacket = DNSPacket.from_packet(packet) on_receive.emit(dns_packet, packet) + +func send_query(packet : DNSPacket): + var raw_packet : StreamPeerBuffer = packet.to_packet() + var byte_array : PackedByteArray = raw_packet.data_array + for client : PacketPeerUDP in clients: + client.put_packet(byte_array) diff --git a/Mods/VRChatOSC/godot-multicast-dns/README.md b/Mods/VRChatOSC/godot-multicast-dns/README.md index 45820f8..6e411fc 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/README.md +++ b/Mods/VRChatOSC/godot-multicast-dns/README.md @@ -2,7 +2,4 @@ This library implements Multicast DNS (MDNS) functionality, primarily receiving MDNS responses. # Features -Interprets query responses and allows easy access. - -# TODO -Sending queries +Interprets query responses and allows easy access, allows for sending DNS Packets in response. From 020998d7a131a5426276ce16d65a413e9efbad5d Mon Sep 17 00:00:00 2001 From: Ellie Date: Sun, 27 Jul 2025 16:32:32 +0930 Subject: [PATCH 10/23] Start work on OSC Query --- Mods/VRChatOSC/OSCQueryServer.gd | 68 ++++ Mods/VRChatOSC/OSCQueryServer.gd.uid | 1 + Mods/VRChatOSC/VRChatOSC.gd | 29 +- Mods/VRChatOSC/godottpd/LICENSE.md | 21 ++ Mods/VRChatOSC/godottpd/http_file_router.gd | 203 +++++++++++ .../godottpd/http_file_router.gd.uid | 1 + Mods/VRChatOSC/godottpd/http_request.gd | 53 +++ Mods/VRChatOSC/godottpd/http_request.gd.uid | 1 + Mods/VRChatOSC/godottpd/http_response.gd | 183 ++++++++++ Mods/VRChatOSC/godottpd/http_response.gd.uid | 1 + Mods/VRChatOSC/godottpd/http_router.gd | 77 +++++ Mods/VRChatOSC/godottpd/http_router.gd.uid | 1 + Mods/VRChatOSC/godottpd/http_server.gd | 318 ++++++++++++++++++ Mods/VRChatOSC/godottpd/http_server.gd.uid | 1 + 14 files changed, 930 insertions(+), 28 deletions(-) create mode 100644 Mods/VRChatOSC/OSCQueryServer.gd create mode 100644 Mods/VRChatOSC/OSCQueryServer.gd.uid create mode 100644 Mods/VRChatOSC/godottpd/LICENSE.md create mode 100644 Mods/VRChatOSC/godottpd/http_file_router.gd create mode 100644 Mods/VRChatOSC/godottpd/http_file_router.gd.uid create mode 100644 Mods/VRChatOSC/godottpd/http_request.gd create mode 100644 Mods/VRChatOSC/godottpd/http_request.gd.uid create mode 100644 Mods/VRChatOSC/godottpd/http_response.gd create mode 100644 Mods/VRChatOSC/godottpd/http_response.gd.uid create mode 100644 Mods/VRChatOSC/godottpd/http_router.gd create mode 100644 Mods/VRChatOSC/godottpd/http_router.gd.uid create mode 100644 Mods/VRChatOSC/godottpd/http_server.gd create mode 100644 Mods/VRChatOSC/godottpd/http_server.gd.uid diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/OSCQueryServer.gd new file mode 100644 index 0000000..eb90e84 --- /dev/null +++ b/Mods/VRChatOSC/OSCQueryServer.gd @@ -0,0 +1,68 @@ +extends Node +class_name OSCQueryServer + +# TODO: Export required? +@export var osc_server : KiriOSCServer +@export var app_name : String +var osc_server_ip : String = "127.0.0.1" +var osc_server_port : int = 9001 + +signal on_host_info_requested + +var http_server : HttpServer +func _ready(): + + osc_server.change_port_and_ip(osc_server_port, osc_server_ip) + osc_server.start() + + var host_info_router = OSCQueryHostInfoRouter.new() + host_info_router.query_server = self + var address_router = OSCQueryAddressRouter.new() + address_router.query_server = self + + http_server = HttpServer.new() + http_server.port = 61613 # TODO: Make random port. This is advertised on mDNS to apps. + add_child(http_server) + http_server.register_router(".*HOST_INFO.*", host_info_router) + http_server.register_router("/", host_info_router) + http_server.start() + +class OSCQueryHostInfoRouter: + extends HttpRouter + var query_server : OSCQueryServer + + func handle_get(request: HttpRequest, response: HttpResponse): + query_server.on_host_info_requested.emit() + var data = { + "NAME": query_server.app_name, + "OSC_IP": query_server.osc_server_ip, + "OSC_PORT": query_server.osc_server_port, + "OSC_TRANSPORT": "UDP", + "EXTENSIONS": { + "ACCESS": true, + "CLIPMODE": false, + "RANGE": true, + "TYPE": true, + "VALUE": true + } + } + var host_info_json = JSON.stringify(data) + response.send(200, host_info_json, "application/json") + +class OSCQueryAddressRouter: + extends HttpRouter + var query_server : OSCQueryServer + + func handle_get(request: HttpRequest, response: HttpResponse): + var data = { + "DESCRIPTION": "", + "FULL_PATH": "/", + "ACCESS": 0, + "TYPE": null, + "CONTENTS": { + "/" + }, + "VALUE": {} + } + var host_info_json = JSON.stringify(data) + response.send(200, host_info_json, "application/json") diff --git a/Mods/VRChatOSC/OSCQueryServer.gd.uid b/Mods/VRChatOSC/OSCQueryServer.gd.uid new file mode 100644 index 0000000..c8929ea --- /dev/null +++ b/Mods/VRChatOSC/OSCQueryServer.gd.uid @@ -0,0 +1 @@ +uid://bg1sitssmsggs diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index c2f268c..9085dea 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -352,32 +352,6 @@ var simplified_parameter_mapping : Dictionary = { }, } -func _apply_transform_rules_a(unified_blendshapes : Dictionary): - # Take in the simplified_parameter_mapping dictionary, - # For each rule in this, identify the rule type then apply the action of the combination type - # Store the resulting value or changes in the unified blendshapes parameter - - # For example: - # EyeLidLeft is a copy of EyeClosedLeft, so it processes the combination type of copy - # Once identified as copy operation, it takes the first combination_shapes item and copies it to last shape - # This copy operation MUST take into consideration what the shape type is. - # You can use the arkit_to_unified_mapping dictionary to map arkit (mediapipe) to unified. - # This dictionary has the key as arkit (mediapipe) and the value as the associated unified shape key. - # You can use the unified_to_arkit_mapping dictionary to map a unified to arkit. - # Current values are always stored in the passed in parameter (unified format) - - # Another example: - # JawX is defined as a value (float) that uses JawRight and JawLeft to form the value - # <0.0 -> 1.0> Jaw Right - # <0.0 -> -1.0> Jaw Left - # Notice the value shifts in direction from positive to negative 1. - # This direction is defined in the particular combination shape, as "direction". - # The final JawX value will reflect the current values of JawRight/JawLeft - # These values must be resolved in the parameter provided to this func. - # All inputs are stored as unified, but shape keys might need to be converted from MediaPipe to Unified. - # See previous example for how to do this. - return {} - func _get_unified_value(shape : String, shape_type : SHAPE_KEY_TYPE, unified_blendshapes : Dictionary) -> float: if shape_type == SHAPE_KEY_TYPE.UNIFIED: return unified_blendshapes.get(shape, 0.0) @@ -463,13 +437,12 @@ func _ready() -> void: add_child(avatar_req) avatar_req.request_completed.connect(_avatar_params_request_complete) dns_service.on_receive.connect(_dns_packet) + for key in unified_to_arkit_mapping: var new_key = unified_to_arkit_mapping[key] var new_value = key arkit_to_unified_mapping[new_key] = new_value -var get_a = true - func _process(delta : float) -> void: if vrchat_osc_query_endpoint == "": diff --git a/Mods/VRChatOSC/godottpd/LICENSE.md b/Mods/VRChatOSC/godottpd/LICENSE.md new file mode 100644 index 0000000..2b717c4 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 deep Entertainment + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Mods/VRChatOSC/godottpd/http_file_router.gd b/Mods/VRChatOSC/godottpd/http_file_router.gd new file mode 100644 index 0000000..f6698a7 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_file_router.gd @@ -0,0 +1,203 @@ +## Class inheriting HttpRouter for handling file serving requests +## +## NOTE: This class mainly handles behind the scenes stuff. +class_name HttpFileRouter +extends HttpRouter + +## Full path to the folder which will be exposed to web +var path: String = "" + +## Relative path to the index page, which will be served when a request is made to "/" (server root) +var index_page: String = "index.html" + +## Relative path to the fallback page which will be served if the requested file was not found +var fallback_page: String = "" + +## An ordered list of extensions that will be checked +## if no file extension is provided by the request +var extensions: PackedStringArray = ["html"] + +## A list of extensions that will be excluded if requested +var exclude_extensions: PackedStringArray = [] + +var weekdays: Array[String] = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] +var monthnames: Array[String] = ['___', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +## Creates an HttpFileRouter intance +## [br] +## [br][param path] - Full path to the folder which will be exposed to web. +## [br][param options] - Optional Dictionary of options which can be configured: +## [br] - [param fallback_page]: Full path to the fallback page which will be served if the requested file was not found +## [br] - [param extensions]: A list of extensions that will be checked if no file extension is provided by the request +## [br] - [param exclude_extensions]: A list of extensions that will be excluded if requested +func _init( + path: String, + options: Dictionary = { + 'index_page': index_page, + 'fallback_page': fallback_page, + 'extensions': extensions, + 'exclude_extensions': exclude_extensions, + } + ) -> void: + self.path = path + self.index_page = options.get("index_page", self.index_page) + self.fallback_page = options.get("fallback_page", self.fallback_page) + self.extensions = options.get("extensions", self.extensions) + self.exclude_extensions = options.get("exclude_extensions", self.exclude_extensions) + +## Handle a GET request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The response to send to the clinet +func handle_get(request: HttpRequest, response: HttpResponse) -> void: + var serving_path: String = path + request.path + var file_exists: bool = _file_exists(serving_path) + + if request.path == "/" and not file_exists: + if index_page.length() > 0: + serving_path = path + "/" + index_page + file_exists = _file_exists(serving_path) + + if request.path.get_extension() == "" and not file_exists: + for extension in extensions: + serving_path = path + request.path + "." + extension + file_exists = _file_exists(serving_path) + if file_exists: + break + + # GDScript must be excluded, unless it is used as a preprocessor (php-like) + if (file_exists and not serving_path.get_extension() in ["gd"] + Array(exclude_extensions)): + var modifiedtime = FileAccess.get_modified_time(serving_path) + var time = Time.get_datetime_dict_from_unix_time(modifiedtime) + var weekday = weekdays[time.weekday] + var monthname = monthnames[time.month] + var timestamp = '%s, %02d %s %04d %02d:%02d:%02d GMT' % [weekday, time.day, monthname, time.year, time.hour, time.minute, time.second] + + if request.headers.get('If-Modified-Since') == timestamp: + response.send_raw(304, ''.to_ascii_buffer(), _get_mime(serving_path.get_extension())) + else: + if request.headers.has('Range'): + var rdata: PackedStringArray = request.headers['Range'].split('=') + var brequest: PackedStringArray = rdata[1].split('-') + if brequest[0].is_valid_int(): + var start: int = brequest[0].to_int() + var file: FileAccess = FileAccess.open(serving_path, FileAccess.READ) + var size = file.get_length() + file.close() + response.send_raw( + 206, + _serve_file(serving_path, start), + _get_mime(serving_path.get_extension()), + "Cache-Control: no-cache\r\nLast-Modified: %s\r\nContent-Range: bytes %s-%s/%s\n\r" % [timestamp, start, size-1, size] + ) + else: + response.send_raw( + 200, + _serve_file(serving_path), + _get_mime(serving_path.get_extension()), + "Cache-Control: no-cache\r\nLast-Modified: %s\r\n" % timestamp + ) + else: + if fallback_page.length() > 0: + serving_path = path + "/" + fallback_page + response.send_raw(200 if index_page == fallback_page else 404, _serve_file(serving_path), _get_mime(fallback_page.get_extension())) + else: + response.send_raw(404) + +# Reads a file as text +# +# #### Parameters +# - file_path: Full path to the file +func _serve_file(file_path: String, seek: int = -1) -> PackedByteArray: + var content: PackedByteArray = [] + var file: FileAccess = FileAccess.open(file_path, FileAccess.READ) + var error = file.get_open_error() + if error: + content = ("Couldn't serve file, ERROR = %s" % error).to_ascii_buffer() + else: + if seek != -1 and seek < file.get_length(): + file.seek(seek) + content = file.get_buffer(file.get_length()) + file.close() + return content + +# Check if a file exists +# +# #### Parameters +# - file_path: Full path to the file +func _file_exists(file_path: String) -> bool: + return FileAccess.file_exists(file_path) + +# Get the full MIME type of a file from its extension +# +# #### Parameters +# - file_extension: Extension of the file to be served +func _get_mime(file_extension: String) -> String: + var type: String = "application" + var subtype : String = "octet-stream" + match file_extension: + # Web files + "css","html","csv","js","mjs": + type = "text" + subtype = "javascript" if file_extension in ["js","mjs"] else file_extension + "php": + subtype = "x-httpd-php" + "ttf","woff","woff2": + type = "font" + subtype = file_extension + # Image + "png","bmp","gif","png","webp": + type = "image" + subtype = file_extension + "jpeg","jpg": + type = "image" + subtype = "jpg" + "tiff", "tif": + type = "image" + subtype = "jpg" + "svg": + type = "image" + subtype = "svg+xml" + "ico": + type = "image" + subtype = "vnd.microsoft.icon" + # Documents + "doc": + subtype = "msword" + "docx": + subtype = "vnd.openxmlformats-officedocument.wordprocessingml.document" + "7z": + subtype = "x-7x-compressed" + "gz": + subtype = "gzip" + "tar": + subtype = "application/x-tar" + "json","pdf","zip": + subtype = file_extension + "txt": + type = "text" + subtype = "plain" + "ppt": + subtype = "vnd.ms-powerpoint" + # Audio + "midi","mp3","wav": + type = "audio" + subtype = file_extension + "mp4","mpeg","webm": + type = "audio" + subtype = file_extension + "oga","ogg": + type = "audio" + subtype = "ogg" + "mpkg": + subtype = "vnd.apple.installer+xml" + # Video + "ogv": + type = "video" + subtype = "ogg" + "avi": + type = "video" + subtype = "x-msvideo" + "ogx": + subtype = "ogg" + return type + "/" + subtype diff --git a/Mods/VRChatOSC/godottpd/http_file_router.gd.uid b/Mods/VRChatOSC/godottpd/http_file_router.gd.uid new file mode 100644 index 0000000..8b47a16 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_file_router.gd.uid @@ -0,0 +1 @@ +uid://d00hhtkp6c38n diff --git a/Mods/VRChatOSC/godottpd/http_request.gd b/Mods/VRChatOSC/godottpd/http_request.gd new file mode 100644 index 0000000..24d41a9 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_request.gd @@ -0,0 +1,53 @@ +## An HTTP request received by the server +class_name HttpRequest +extends RefCounted + + +## A dictionary of the headers of the request +var headers: Dictionary + +## The received raw body +var body: String + +## A match object of the regular expression that matches the path +var query_match: RegExMatch + +## The path that matches the router path +var path: String + +## The method +var method: String + +## A dictionary of request (aka. routing) parameters +var parameters: Dictionary + +## A dictionary of request query parameters +var query: Dictionary + +## Returns the body object based on the raw body and the content type of the request +func get_body_parsed() -> Variant: + var content_type: String = "" + + if(headers.has("content-type")): + content_type = headers["content-type"] + elif(headers.has("Content-Type")): + content_type = headers["Content-Type"] + + if(content_type == "application/json"): + return JSON.parse_string(body) + + if(content_type == "application/x-www-form-urlencoded"): + var data = {} + + for body_part in body.split("&"): + var key_and_value = body_part.split("=") + data[key_and_value[0]] = key_and_value[1] + + return data + + # Not supported contenty type parsing... for now + return null + +## Override `str()` method, automatically called in `print()` function +func _to_string() -> String: + return JSON.stringify({headers=headers, method=method, path=path}) diff --git a/Mods/VRChatOSC/godottpd/http_request.gd.uid b/Mods/VRChatOSC/godottpd/http_request.gd.uid new file mode 100644 index 0000000..ad460e8 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_request.gd.uid @@ -0,0 +1 @@ +uid://csngl33dupj6u diff --git a/Mods/VRChatOSC/godottpd/http_response.gd b/Mods/VRChatOSC/godottpd/http_response.gd new file mode 100644 index 0000000..32f8e73 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_response.gd @@ -0,0 +1,183 @@ +## A response object useful to send out responses +class_name HttpResponse +extends RefCounted + + +## The client currently talking to the server +var client: StreamPeer + +## The server identifier to use on responses [GodotTPD] +var server_identifier: String = "GodotTPD" + +## A dictionary of headers +## [br] Headers can be set using the `set(name, value)` function +var headers: Dictionary = {} + +## An array of cookies +## [br] Cookies can be set using the `cookie(name, value, options)` function +## [br] Cookies will be automatically sent via "Set-Cookie" headers to clients +var cookies: Array = [] + +## Origins allowed to call this resource +var access_control_origin = "*" + +## Comma separed methods for the access control +var access_control_allowed_methods = "POST, GET, OPTIONS" + +## Comma separed headers for the access control +var access_control_allowed_headers = "content-type" + +## Send out a raw (Bytes) response to the client +## [br]Useful to send files faster or raw data which will be converted by the client +## [br][param status] - The HTTP Status code to send +## [br][param data] - The body data to send +## [br][param content_type] - The type of content to send. +func send_raw(status_code: int, data: PackedByteArray = PackedByteArray([]), content_type: String = "application/octet-stream", extra_header: String = "") -> void: + client.put_data(("HTTP/1.1 %d %s\r\n" % [status_code, _match_status_code(status_code)]).to_ascii_buffer()) + client.put_data(("Server: %s\r\n" % server_identifier).to_ascii_buffer()) + for header in headers.keys(): + client.put_data(("%s: %s\r\n" % [header, headers[header]]).to_ascii_buffer()) + for cookie in cookies: + client.put_data(("Set-Cookie: %s\r\n" % cookie).to_ascii_buffer()) + client.put_data(("Content-Length: %d\r\n" % data.size()).to_ascii_buffer()) + client.put_data("Connection: close\r\n".to_ascii_buffer()) + client.put_data(("Access-Control-Allow-Origin: %s\r\n" % access_control_origin).to_ascii_buffer()) + client.put_data(("Access-Control-Allow-Methods: %s\r\n" % access_control_allowed_methods).to_ascii_buffer()) + client.put_data(("Access-Control-Allow-Headers: %s\r\n" % access_control_allowed_headers).to_ascii_buffer()) + client.put_data("Accept-Ranges: bytes\r\n".to_ascii_buffer()) + client.put_data(extra_header.to_ascii_buffer()) + client.put_data(("Content-Type: %s\r\n\r\n" % content_type).to_ascii_buffer()) + + client.put_data(data) + +## For sending parts of data +## [br]TODO: http_file_router.gd - use this to send small parts of large files at a time to avoid smashing the ram of the server +## [br]TODO: This will probably be used for range header? +func send_partial(status_code: int, data: PackedByteArray = PackedByteArray([]), content_type: String = "application/octet-stream", extra_header: String = "") -> void: + client.put_data(data) + +## Send out a response to the client +## [br] +## [br][param status_code] - The HTTP status code to send +## [br][param data] - The body to send +## [br][param content_type] - The type of the content to send +func send(status_code: int, data: String = "", content_type = "text/html") -> void: + send_raw(status_code, data.to_ascii_buffer(), content_type) + +## Send out a JSON response to the client +## [br] This function will internally call the [method send] +## [br] +## [br][param status_code] - The HTTP status code to send +## [br][param data] - The body to send +func json(status_code: int, data) -> void: + send(status_code, JSON.stringify(data), "application/json") + + +## Sets the response’s header "field" to "value" +## [br] +## [br][param field] - The name of the header. i.e. [code]Accept-Type[/code] +## [br][param value] - The value of this header. i.e. [code]application/json[/code] +func set(field: StringName, value: Variant) -> void: + headers[field] = value + + +## Sets cookie "name" to "value" +## [br] +## [br][param name] - The name of the cookie. i.e. [code]user-id[/code] +## [br][param value] - The value of this cookie. i.e. [code]abcdef[/code] +## [br][param options] - A Dictionary of [url=https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes]cookie attributes[/url] +## for this specific cokkie in the [code]{ "secure" : "true"}[/code] format. +func cookie(name: String, value: String, options: Dictionary = {}) -> void: + var cookie: String = name+"="+value + if options.has("domain"): cookie+="; Domain="+options["domain"] + if options.has("max-age"): cookie+="; Max-Age="+options["max-age"] + if options.has("expires"): cookie+="; Expires="+options["expires"] + if options.has("path"): cookie+="; Path="+options["path"] + if options.has("secure"): cookie+="; Secure="+options["secure"] + if options.has("httpOnly"): cookie+="; HttpOnly="+options["httpOnly"] + if options.has("sameSite"): + match (options["sameSite"]): + true: cookie += "; SameSite=Strict" + "lax": cookie += "; SameSite=Lax" + "strict": cookie += "; SameSite=Strict" + "none": cookie += "; SameSite=None" + _: pass + cookies.append(cookie) + + +## Automatically matches a "status_code" to an RFC 7231 compliant "status_text" +## [br] +## [br][param code] - The HTTP Status code to be matched +## [br]Returns: the matched [code]status_text[/code] +func _match_status_code(code: int) -> String: + var text: String = "OK" + match(code): + # 1xx - Informational Responses + 100: text="Continue" + 101: text="Switching protocols" + 102: text="Processing" + 103: text="Early Hints" + # 2xx - Successful Responses + 200: text="OK" + 201: text="Created" + 202: text="Accepted" + 203: text="Non-Authoritative Information" + 204: text="No Content" + 205: text="Reset Content" + 206: text="Partial Content" + 207: text="Multi-Status" + 208: text="Already Reported" + 226: text="IM Used" + # 3xx - Redirection Messages + 300: text="Multiple Choices" + 301: text="Moved Permanently" + 302: text="Found (Previously 'Moved Temporarily')" + 303: text="See Other" + 304: text="Not Modified" + 305: text="Use Proxy" + 306: text="Switch Proxy" + 307: text="Temporary Redirect" + 308: text="Permanent Redirect" + # 4xx - Client Error Responses + 400: text="Bad Request" + 401: text="Unauthorized" + 402: text="Payment Required" + 403: text="Forbidden" + 404: text="Not Found" + 405: text="Method Not Allowed" + 406: text="Not Acceptable" + 407: text="Proxy Authentication Required" + 408: text="Request Timeout" + 409: text="Conflict" + 410: text="Gone" + 411: text="Length Required" + 412: text="Precondition Failed" + 413: text="Payload Too Large" + 414: text="URI Too Long" + 415: text="Unsupported Media Type" + 416: text="Range Not Satisfiable" + 417: text="Expectation Failed" + 418: text="I'm a Teapot" + 421: text="Misdirected Request" + 422: text="Unprocessable Entity" + 423: text="Locked" + 424: text="Failed Dependency" + 425: text="Too Early" + 426: text="Upgrade Required" + 428: text="Precondition Required" + 429: text="Too Many Requests" + 431: text="Request Header Fields Too Large" + 451: text="Unavailable For Legal Reasons" + # 5xx - Server Error Responses + 500: text="Internal Server Error" + 501: text="Not Implemented" + 502: text="Bad Gateway" + 503: text="Service Unavailable" + 504: text="Gateway Timeout" + 505: text="HTTP Version Not Supported" + 506: text="Variant Also Negotiates" + 507: text="Insufficient Storage" + 508: text="Loop Detected" + 510: text="Not Extended" + 511: text="Network Authentication Required" + return text diff --git a/Mods/VRChatOSC/godottpd/http_response.gd.uid b/Mods/VRChatOSC/godottpd/http_response.gd.uid new file mode 100644 index 0000000..41d0e0f --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_response.gd.uid @@ -0,0 +1 @@ +uid://s3kas615o7k5 diff --git a/Mods/VRChatOSC/godottpd/http_router.gd b/Mods/VRChatOSC/godottpd/http_router.gd new file mode 100644 index 0000000..6c67839 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_router.gd @@ -0,0 +1,77 @@ +## A base class for all HTTP routers +## +## This router handles all the requests that the client sends to the server. +## [br]NOTE: This class is meant to be expanded upon instead of used directly. +## [br]Usage: +## [codeblock] +## class_name MyCustomRouter +## extends HttpRouter +## +## func handle_get(request: HttpRequest, response: HttpResponse) -> void: +## response.send(200, "Hello World") +## [/codeblock] +class_name HttpRouter +extends RefCounted + + +## Handle a GET request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_get(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "GET not allowed") + + +## Handle a POST request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_post(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "POST not allowed") + + +## Handle a HEAD request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_head(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "HEAD not allowed") + + +## Handle a PUT request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_put(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "PUT not allowed") + + +## Handle a PATCH request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_patch(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "PATCH not allowed") + + +## Handle a DELETE request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_delete(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "DELETE not allowed") + + +## Handle an OPTIONS request +## [br] +## [br][param request] - The request from the client +## [br][param response] - The node to send the response back to the client +@warning_ignore("unused_parameter") +func handle_options(request: HttpRequest, response: HttpResponse) -> void: + response.send(405, "OPTIONS not allowed") diff --git a/Mods/VRChatOSC/godottpd/http_router.gd.uid b/Mods/VRChatOSC/godottpd/http_router.gd.uid new file mode 100644 index 0000000..3ed38f9 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_router.gd.uid @@ -0,0 +1 @@ +uid://b6210q0cp5u8a diff --git a/Mods/VRChatOSC/godottpd/http_server.gd b/Mods/VRChatOSC/godottpd/http_server.gd new file mode 100644 index 0000000..b3a85a6 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_server.gd @@ -0,0 +1,318 @@ +## A routable HTTP server for Godot +## +## Provides a web server with routes for specific endpoints +## [br]Example usage: +## [codeblock] +## var server := HttpServer.new() +## server.register_router("/", MyExampleRouter.new()) +## add_child(server) +## server.start() +## [/codeblock] + +class_name HttpServer +extends Node + +## The ip address to bind the server to. Use * for all IP addresses [*] +var bind_address: String = "*" + +## The port to bind the server to. [8080] +var port: int = 8080 + +## The server identifier to use when responding to requests [GodotTPD] +var server_identifier: String = "GodotTPD" + +# If `HttpRequest`s and `HttpResponse`s should be logged +var _logging: bool = false + +# The TCP server instance used +var _server: TCPServer + +# An array of StraemPeerTCP objects who are currently talking to the server +var _clients: Array + +# A list of HttpRequest routers who could handle a request +var _routers: Array = [] + +# A regex identifiying the method line +var _method_regex: RegEx = RegEx.new() + +# A regex for header lines +var _header_regex: RegEx = RegEx.new() + +# The base path used in a project to serve files +var _local_base_path: String = "res://src" + +# list of host allowed to call the server +var _allowed_origins: PackedStringArray = [] + +# Comma separed methods for the access control +var _access_control_allowed_methods = "POST, GET, OPTIONS" + +# Comma separed headers for the access control +var _access_control_allowed_headers = "content-type" + +# Compile the required regex +func _init(_logging: bool = false): + self._logging = _logging + set_process(false) + _method_regex.compile("^(?GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS) (?[^ ]+) HTTP/1.1$") + _header_regex.compile("^(?[\\w-]+): (?(.*))$") + +# Print a debug message in console, if the debug mode is enabled +# +# #### Parameters +# - message: The message to be printed (only in debug mode) +func _print_debug(message: String) -> void: + var time = Time.get_datetime_dict_from_system() + var time_return = "%02d-%02d-%02d %02d:%02d:%02d" % [time.year, time.month, time.day, time.hour, time.minute, time.second] + print("[SERVER] ",time_return," >> ", message) + +## Register a new router to handle a specific path +## [br] +## [br][param path] - The path the router will handle. +## Supports a regular expression and the group matches will be available in HttpRequest.query_match. +## [br][param router] - The router which will handle the request +func register_router(path: String, router: HttpRouter, condition: Callable = func(request: HttpRequest): return true): + var path_regex = RegEx.new() + var params: Array = [] + if path.left(0) == "^": + path_regex.compile(path) + else: + var regexp: Array = _path_to_regexp(path, router is HttpFileRouter) + path_regex.compile(regexp[0]) + params = regexp[1] + _routers.push_back({ + "path": path_regex, + "params": params, + "router": router, + "condition": condition, + }) + + +## Handle possibly incoming requests +func _process(_delta: float) -> void: + if _server: + while _server.is_connection_available(): + var new_client = _server.take_connection() + if new_client: + self._clients.append(new_client) + for client in self._clients: + client.poll() + if client.get_status() == StreamPeerTCP.STATUS_CONNECTED: + var bytes = client.get_available_bytes() + if bytes > 0: + var request_string = client.get_utf8_string(bytes) + self._handle_request(client, request_string) + _remove_disconnected_clients() + + +func _remove_disconnected_clients(): + var valid_statuses = [StreamPeerTCP.STATUS_CONNECTED, StreamPeerTCP.STATUS_CONNECTING] + self._clients = self._clients.filter( + func(c: StreamPeerTCP): return valid_statuses.has(c.get_status()) + ) + + +## Start the server +func start(): + set_process(true) + self._server = TCPServer.new() + var err: int = self._server.listen(self.port, self.bind_address) + match err: + 22: + _print_debug("Could not bind to port %d, already in use" % [self.port]) + stop() + _: + _print_debug("HTTP Server listening on http://%s:%s" % [self.bind_address, self.port]) + + +## Stop the server and disconnect all clients +func stop(): + for client in self._clients: + client.disconnect_from_host() + self._clients.clear() + self._server.stop() + set_process(false) + _print_debug("Server stopped.") + + +# Interpret a request string and perform the request +# +# #### Parameters +# - client: The client that send the request +# - request: The received request as a String +func _handle_request(client: StreamPeer, request_string: String): + var request = HttpRequest.new() + for line in request_string.split("\r\n"): + var method_matches = _method_regex.search(line) + var header_matches = _header_regex.search(line) + if method_matches: + request.method = method_matches.get_string("method") + var request_path: String = method_matches.get_string("path") + # Check if request_path contains "?" character, could be a query parameter + if not "?" in request_path: + request.path = request_path + else: + var path_query: PackedStringArray = request_path.split("?") + request.path = path_query[0] + request.query = _extract_query_params(path_query[1]) + request.headers = {} + request.body = "" + elif header_matches: + request.headers[header_matches.get_string("key")] = \ + header_matches.get_string("value") + else: + request.body += line + self._perform_current_request(client, request) + + +# Handle a specific request and send it to a router +# If no router matches, send a 404 +# +# #### Parameters +# - client: The client that send the request +# - request_info: A dictionary with information about the request +# - method: The method of the request (e.g. GET, POST) +# - path: The requested path +# - headers: A dictionary of headers of the request +# - body: The raw body of the request +func _perform_current_request(client: StreamPeer, request: HttpRequest): + var thread = Thread.new() + thread.start(__perform_current_request.bind(client, request)) + +func __perform_current_request(client: StreamPeer, request: HttpRequest): + _print_debug("HTTP Request: " + str(request)) + var found = false + var is_allowed_origin = false + var response = HttpResponse.new() + var fetch_mode = "" + var origin = "" + response.client = client + response.server_identifier = server_identifier + + if request.headers.has("Sec-Fetch-Mode"): + fetch_mode = request.headers["Sec-Fetch-Mode"] + elif request.headers.has("sec-fetch-mode"): + fetch_mode = request.headers["sec-fetch-mode"] + + if request.headers.has("Origin"): + origin = request.headers["Origin"] + elif request.headers.has("origin"): + origin = request.headers["origin"] + + if _allowed_origins.has(origin): + is_allowed_origin = true + response.access_control_origin = origin + + response.access_control_allowed_methods = _access_control_allowed_methods + response.access_control_allowed_headers = _access_control_allowed_headers + + for router in self._routers: + if not router.condition.bind(request).call(): break + + var matches = router.path.search(request.path) + if matches: + request.query_match = matches + if request.query_match.get_string("subpath"): + request.path = request.query_match.get_string("subpath") + if router.params.size() > 0: + for parameter in router.params: + request.parameters[parameter] = request.query_match.get_string(parameter) + match request.method: + "GET": + found = true + router.router.handle_get(request, response) + "POST": + found = true + router.router.handle_post(request, response) + "HEAD": + found = true + router.router.handle_head(request, response) + "PUT": + found = true + router.router.handle_put(request, response) + "PATCH": + found = true + router.router.handle_patch(request, response) + "DELETE": + found = true + router.router.handle_delete(request, response) + "OPTIONS": + if _allowed_origins.size() > 0 && fetch_mode == "cors": + if is_allowed_origin: + response.send(204) + else: + response.send(400, "%s is not present in the allowed origins" % origin) + + return + + found = true + router.router.handle_options(request, response) + break + if not found: + response.send(404, "Not found") + + +# Converts a URL path to @regexp RegExp, providing a mechanism to fetch groups from the expression +# indexing each parameter by name in the @params array +# +# #### Parameters +# - path: The path of the HttpRequest +# - should_match_subfolder: (dafult [false]) if subfolders should be matched and grouped, +# used for HttpFileRouter +# +# Returns: A 2D array, containing a @regexp String and Dictionary of @params +# [0] = @regexp --> the output expression as a String, to be compiled in RegExp +# [1] = @params --> an Array of parameters, indexed by names +# ex. "/user/:id" --> "^/user/(?([^/#?]+?))[/#?]?$" +func _path_to_regexp(path: String, should_match_subfolders: bool = false) -> Array: + var regexp: String = "^" + var params: Array = [] + var fragments: Array = path.split("/") + fragments.pop_front() + for fragment in fragments: + if fragment.left(1) == ":": + fragment = fragment.lstrip(":") + regexp += "/(?<%s>([^/#?]+?))" % fragment + params.append(fragment) + else: + regexp += "/" + fragment + regexp += "[/#?]?$" if not should_match_subfolders else "(?$|/.*)" + return [regexp, params] + + +## Enable CORS (Cross-origin resource sharing) which only allows requests from the specified servers +## [br] +## [br][param allowed_origins] - The origins that are allowed to be accessed from this server +## [br][param access_control_allowed_methods] - The methods that are allowed to be used +## [br][param access_control_allowed_headers] - The headers that are allowed to be sent +func enable_cors(allowed_origins: PackedStringArray, access_control_allowed_methods : String = "POST, GET, OPTIONS", access_control_allowed_headers : String = "content-type"): + _allowed_origins = allowed_origins + _access_control_allowed_methods = access_control_allowed_methods + _access_control_allowed_headers = access_control_allowed_headers + + +# Extracts query parameters from a String query, +# building a Query Dictionary of param:value pairs +# +# #### Parameters +# - query_string: the query string, extracted from the HttpRequest.path +# +# Returns: A Dictionary of param:value pairs +func _extract_query_params(query_string: String) -> Dictionary: + var query: Dictionary = {} + if query_string == "": + return query + var parameters: Array = query_string.split("&") + for param in parameters: + if not "=" in param: + continue + var kv : Array = param.split("=") + var value: String = kv[1] + if value.is_valid_int(): + query[kv[0]] = value.to_int() + elif value.is_valid_float(): + query[kv[0]] = value.to_float() + else: + query[kv[0]] = value + return query diff --git a/Mods/VRChatOSC/godottpd/http_server.gd.uid b/Mods/VRChatOSC/godottpd/http_server.gd.uid new file mode 100644 index 0000000..20902e6 --- /dev/null +++ b/Mods/VRChatOSC/godottpd/http_server.gd.uid @@ -0,0 +1 @@ +uid://b616mq2u5dtyn From 98b781db123dd5fa2d2c3e3b905af9039bbfa808 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 27 Sep 2025 18:41:53 +0930 Subject: [PATCH 11/23] Add OSC Query and OSC Server support This enables VRChat to alert SnekStudio when the VRChat avatar changes. --- .../LICENSE.godottpd.md | 0 Mods/VRChatOSC/OSCQueryServer.gd | 35 +- Mods/VRChatOSC/VRCFTParameters.gd | 300 ++++++++++++ Mods/VRChatOSC/VRCFTParameters.gd.uid | 1 + Mods/VRChatOSC/VRCParams.gd | 2 + Mods/VRChatOSC/VRChatOSC.gd | 441 +++++------------- Mods/VRChatOSC/VRChatOSC.tscn | 17 +- .../godot-multicast-dns/DNSRecord.gd | 4 +- .../godot-multicast-dns/MulticastDNS.gd | 3 +- Mods/VRChatOSC/godot-multicast-dns/README.md | 2 +- Mods/VRChatOSC/godottpd/http_response.gd | 4 +- 11 files changed, 469 insertions(+), 340 deletions(-) rename Mods/VRChatOSC/godottpd/LICENSE.md => Licenses/LICENSE.godottpd.md (100%) create mode 100644 Mods/VRChatOSC/VRCFTParameters.gd create mode 100644 Mods/VRChatOSC/VRCFTParameters.gd.uid diff --git a/Mods/VRChatOSC/godottpd/LICENSE.md b/Licenses/LICENSE.godottpd.md similarity index 100% rename from Mods/VRChatOSC/godottpd/LICENSE.md rename to Licenses/LICENSE.godottpd.md diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/OSCQueryServer.gd index eb90e84..a386d81 100644 --- a/Mods/VRChatOSC/OSCQueryServer.gd +++ b/Mods/VRChatOSC/OSCQueryServer.gd @@ -4,16 +4,19 @@ class_name OSCQueryServer # TODO: Export required? @export var osc_server : KiriOSCServer @export var app_name : String -var osc_server_ip : String = "127.0.0.1" -var osc_server_port : int = 9001 +@export var osc_paths : Dictionary = {} +@export var osc_server_ip : String = "127.0.0.1" +@export var osc_server_port : int = 9001 signal on_host_info_requested +signal on_root_requested +signal on_osc_server_message_received(address : String, args) -var http_server : HttpServer +@export var http_server : HttpServer func _ready(): - osc_server.change_port_and_ip(osc_server_port, osc_server_ip) - osc_server.start() + osc_server.message_received.connect(_message_received) + osc_server.start_server() var host_info_router = OSCQueryHostInfoRouter.new() host_info_router.query_server = self @@ -21,12 +24,17 @@ func _ready(): address_router.query_server = self http_server = HttpServer.new() + http_server.bind_address = "127.0.0.1" http_server.port = 61613 # TODO: Make random port. This is advertised on mDNS to apps. add_child(http_server) - http_server.register_router(".*HOST_INFO.*", host_info_router) - http_server.register_router("/", host_info_router) + + http_server.register_router("^/HOST_INFO", host_info_router) + http_server.register_router("^/", address_router) http_server.start() +func _message_received(address : String, args) -> void: + on_osc_server_message_received.emit(address, args) + class OSCQueryHostInfoRouter: extends HttpRouter var query_server : OSCQueryServer @@ -54,15 +62,12 @@ class OSCQueryAddressRouter: var query_server : OSCQueryServer func handle_get(request: HttpRequest, response: HttpResponse): + query_server.on_root_requested.emit() var data = { - "DESCRIPTION": "", + "DESCRIPTION": "Root", "FULL_PATH": "/", "ACCESS": 0, - "TYPE": null, - "CONTENTS": { - "/" - }, - "VALUE": {} + "CONTENTS": query_server.osc_paths, } - var host_info_json = JSON.stringify(data) - response.send(200, host_info_json, "application/json") + var root_json = JSON.stringify(data) + response.send(200, root_json, "application/json") diff --git a/Mods/VRChatOSC/VRCFTParameters.gd b/Mods/VRChatOSC/VRCFTParameters.gd new file mode 100644 index 0000000..0e36ae0 --- /dev/null +++ b/Mods/VRChatOSC/VRCFTParameters.gd @@ -0,0 +1,300 @@ +class_name ParameterMappings +extends Node + +enum COMBINATION_TYPE { + RANGE = 1, + COPY = 2, + AVERAGE = 3, + WEIGHTED = 4 +} +enum SHAPE_KEY_TYPE { + MEDIAPIPE = 1, + UNIFIED = 2 +} +enum DIRECTION { + POSITIVE = 1, + NEGATIVE = 2 +} + +static var simplified_parameter_mapping : Dictionary = { + "BrowUpRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowUpLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowDownRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "BrowDownLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawZ": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawForward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawBackward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLidRight": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLidLeft": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLid": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeRightX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookOutRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookInRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeRightY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookInLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookOutLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeX": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, + "EyeY": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, +} + +static var legacy_parameter_mapping : Dictionary = { + "JawOpenSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekSuck", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, +} diff --git a/Mods/VRChatOSC/VRCFTParameters.gd.uid b/Mods/VRChatOSC/VRCFTParameters.gd.uid new file mode 100644 index 0000000..a7d8e37 --- /dev/null +++ b/Mods/VRChatOSC/VRCFTParameters.gd.uid @@ -0,0 +1 @@ +uid://nmmdiw1kj11l diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index 87c14f4..5c1c131 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -13,6 +13,8 @@ func reset(): _has_changed_avi = false _avatar_id = "" _raw_params = {} + _float_params = {} + _binary_params = {} func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_avi : bool): _raw_params = raw_avatar_params diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index 9085dea..e104eab 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -4,7 +4,9 @@ class_name VRChatOSC @export var dns_service : MulticastDNS @export var update_vrc_param_values : bool = false @export var osc_client : KiriOSClient - +@export var osc_query_server : OSCQueryServer +var osc_query_name : String = str(randi_range(500000, 5000000)) +var osc_server_name : String = str(randi_range(500000, 5000000)) var vrchat_osc_query_endpoint : String = "" ## The current value of the avatar ID. var current_avatar_id : String @@ -73,311 +75,34 @@ var unified_to_arkit_mapping : Dictionary = { "TongueOut": "tongueOut" } var arkit_to_unified_mapping : Dictionary = {} -enum COMBINATION_TYPE { - RANGE = 1, - COPY = 2, - AVERAGE = 3, - WEIGHTED = 4 -} -enum SHAPE_KEY_TYPE { - MEDIAPIPE = 1, - UNIFIED = 2 -} -enum DIRECTION { - POSITIVE = 1, - NEGATIVE = 2 -} -var simplified_parameter_mapping : Dictionary = { - "BrowUpRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowOuterUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 - }, - { - "shape": "BrowInnerUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, - ] - }, - "BrowUpLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowOuterUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 - }, - { - "shape": "BrowInnerUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, - ] - }, - "BrowDownRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowLowererRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 - }, - { - "shape": "BrowPinchRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 - }, - ] - }, - "BrowDownLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowLowererLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 - }, - { - "shape": "BrowPinchLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 - }, - ] - }, - "MouthSmileRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "MouthCornerPullRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 - }, - { - "shape": "MouthCornerSlantRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 - }, - ] - }, - "MouthSmileLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "MouthCornerPullLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 - }, - { - "shape": "MouthCornerSlantLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 - }, - ] - }, - "MouthX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "JawX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "JawRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "JawLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "JawZ": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "JawForward", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "JawBackward", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "EyeLidRight": { - "combination_type": COMBINATION_TYPE.COPY, - "combination_shapes": [ - { - "shape": "EyeClosedRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "inverse": true - }, - { - "shape": "EyeLidRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - ] - }, - "EyeLidLeft": { - "combination_type": COMBINATION_TYPE.COPY, - "combination_shapes": [ - { - "shape": "EyeClosedLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "inverse": true - }, - { - "shape": "EyeLidLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - ] - }, - "EyeLid": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "EyeLidLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - { - "shape": "EyeLidRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - ] - }, - "EyeRightX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "EyeLookOutRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "EyeLookInRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "EyeRightY": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "EyeLookUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "EyeLookDownRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "EyeLeftX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "EyeLookInLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "EyeLookOutLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "EyeLeftY": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "EyeLookUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "EyeLookDownLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "EyeX": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "EyeRightX", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "EyeLeftX", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - ] - }, - "EyeY": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "EyeRightY", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "EyeLeftY", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - ] - }, -} -func _get_unified_value(shape : String, shape_type : SHAPE_KEY_TYPE, unified_blendshapes : Dictionary) -> float: - if shape_type == SHAPE_KEY_TYPE.UNIFIED: +func _get_unified_value(shape : String, shape_type : ParameterMappings.SHAPE_KEY_TYPE, unified_blendshapes : Dictionary) -> float: + if shape_type == ParameterMappings.SHAPE_KEY_TYPE.UNIFIED: return unified_blendshapes.get(shape, 0.0) - elif shape_type == SHAPE_KEY_TYPE.MEDIAPIPE: + elif shape_type == ParameterMappings.SHAPE_KEY_TYPE.MEDIAPIPE: var unified_shape: String = arkit_to_unified_mapping.get(shape, shape) return unified_blendshapes.get(unified_shape, 0.0) return 0.0 -func _get_unified_shape(shape: String, shape_type: SHAPE_KEY_TYPE) -> String: - if shape_type == SHAPE_KEY_TYPE.UNIFIED: +func _get_unified_shape(shape: String, shape_type: ParameterMappings.SHAPE_KEY_TYPE) -> String: + if shape_type == ParameterMappings.SHAPE_KEY_TYPE.UNIFIED: + return shape - elif shape_type == SHAPE_KEY_TYPE.MEDIAPIPE: + elif shape_type == ParameterMappings.SHAPE_KEY_TYPE.MEDIAPIPE: return arkit_to_unified_mapping.get(shape, shape) return shape -func _apply_transform_rules(unified_blendshapes : Dictionary) -> void: - for param_name : String in simplified_parameter_mapping.keys(): - var rule : Dictionary = simplified_parameter_mapping[param_name] +func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictionary) -> void: + for param_name : String in base_dict.keys(): + var rule : Dictionary = base_dict[param_name] var comb_type : int = rule["combination_type"] var shapes : Array = rule["combination_shapes"] match comb_type: - COMBINATION_TYPE.COPY: + ParameterMappings.COMBINATION_TYPE.COPY: var src_shape_info : Dictionary = shapes[0] var src_shape : String = src_shape_info["shape"] - var src_type : SHAPE_KEY_TYPE = src_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var src_type : ParameterMappings.SHAPE_KEY_TYPE = src_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var src_value : float = _get_unified_value(src_shape, src_type, unified_blendshapes) var src_inverse : bool = src_shape_info.get("inverse", false) if src_inverse: @@ -389,44 +114,44 @@ func _apply_transform_rules(unified_blendshapes : Dictionary) -> void: for i in range(1, shapes.size()): var dst_shape_info : Dictionary = shapes[i] var dst_shape : String = dst_shape_info["shape"] - var dst_type : SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var dst_type : ParameterMappings.SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var unified_shape : String = _get_unified_shape(dst_shape, dst_type) unified_blendshapes[unified_shape] = src_value - COMBINATION_TYPE.AVERAGE: + ParameterMappings.COMBINATION_TYPE.AVERAGE: var sum : float = 0.0 var count : int = 0 for shape_info : Dictionary in shapes: var shape : String = shape_info["shape"] - var shape_type : SHAPE_KEY_TYPE = shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) sum += value count += 1 unified_blendshapes[param_name] = sum / max(count, 1) - COMBINATION_TYPE.RANGE: + ParameterMappings.COMBINATION_TYPE.RANGE: var total : float = 0.0 for shape_info : Dictionary in shapes: var shape : String = shape_info["shape"] - var shape_type : SHAPE_KEY_TYPE = shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) - var direction : DIRECTION = shape_info.get("direction", DIRECTION.POSITIVE) + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var direction : ParameterMappings.DIRECTION = shape_info.get("direction", ParameterMappings.DIRECTION.POSITIVE) var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) - if direction == DIRECTION.POSITIVE: + if direction == ParameterMappings.DIRECTION.POSITIVE: total += value else: total -= value unified_blendshapes[param_name] = total - COMBINATION_TYPE.WEIGHTED: + ParameterMappings.COMBINATION_TYPE.WEIGHTED: var src_shape_info : Dictionary = shapes[0] var src_shape : String = src_shape_info["shape"] - var src_type : SHAPE_KEY_TYPE = src_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var src_type : ParameterMappings.SHAPE_KEY_TYPE = src_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var src_value : float = _get_unified_value(src_shape, src_type, unified_blendshapes) var src_weight : float = src_shape_info["weight"] var dst_shape_info : Dictionary = shapes[1] var dst_shape : String = dst_shape_info["shape"] - var dst_type : SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", SHAPE_KEY_TYPE.UNIFIED) + var dst_type : ParameterMappings.SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var dst_value : float = _get_unified_value(dst_shape, dst_type, unified_blendshapes) var dst_weight : float = dst_shape_info["weight"] @@ -436,7 +161,19 @@ func _ready() -> void: avatar_req = HTTPRequest.new() add_child(avatar_req) avatar_req.request_completed.connect(_avatar_params_request_complete) - dns_service.on_receive.connect(_dns_packet) + # We need to know the vrc endpoint to get data from. + dns_service.on_receive.connect(_vrc_dns_packet) + # We need to have another connection to resolve OTHER DNS queries (OSCQuery). + dns_service.on_receive.connect(_resolve_dns_packet) + osc_query_server.osc_paths = { + "/avatar/change": { + "DESCRIPTION": "Avatar Change", + "FULL_PATH": "/avatar/change", + "ACCESS": 2, # WRITE_ONLY + "TYPE": "s", + } + } + osc_query_server.on_osc_server_message_received.connect(_osc_query_received) for key in unified_to_arkit_mapping: var new_key = unified_to_arkit_mapping[key] @@ -455,20 +192,11 @@ func _process(delta : float) -> void: # Map the blendshapes we have from mediapipe to the unified versions. var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() - _apply_transform_rules(unified_blendshapes) - - # Handle mapping simplified parameters - - # TODO: Eye parameters - - # TODO: Brow parameters - - # TODO: Mouth parameters - - # TODO: Lip parameters - - # TODO: Nose and cheek parameters + # Apply unified blendshape simplification mapping + _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) + # Apply legacy parameter mapping (this makes me sad) + _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) # Set params to values for shape in unified_blendshapes: @@ -504,8 +232,80 @@ func _send_dirty_params(): type = "F" osc_client.send_osc_message(param.full_path, type, [param.value]) +func _osc_query_received(address : String, args) -> void: + if address == "/avatar/change": + print("WAOH") + _get_avatar_params() + +func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: + if vrchat_osc_query_endpoint == "" or packet.opcode != 0: + return + + for question : DNSQuestion in packet.dns_questions: + # We have two services to respond to: + # 1. The OSC Query Server (http) (_oscjson._tcp.local) + # 2. The OSC Server (udp) (_osc._udp.local) + var service_name : String = "" + var is_osc_query : bool = false + if question.full_label.begins_with("_osc._udp.local"): + service_name = "SNEKS-" + osc_server_name + elif question.full_label.begins_with("_oscjson._tcp.local"): + service_name = "SNEKS-" + osc_query_name + is_osc_query = true + + if service_name == "": + continue + + var full_name : Array[String] = [service_name, question.labels[0], question.labels[1], question.labels[2]] + var full_service_name : Array[String] = [service_name, question.labels[0].replace("_", ""), question.labels[1].replace("_", "")] + + var txt_record = DNSRecord.new() -func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: + txt_record.labels = full_name + txt_record.dns_type = DNSRecord.RECORD_TYPE.TXT + txt_record.data = { "text": "txtvers=1" } + + var srv_record = DNSRecord.new() + srv_record.labels = full_name + srv_record.dns_type = DNSRecord.RECORD_TYPE.SRV + if is_osc_query: + srv_record.data = { "port": osc_query_server.http_server.port } + else: + srv_record.data = { "port": osc_query_server.osc_server_port } + srv_record.data.set("target", full_service_name) + + var a_record = DNSRecord.new() + a_record.dns_type = DNSRecord.RECORD_TYPE.A + a_record.labels = full_service_name + # We know this will always be 127.0.0.1 buuuuuuuuut + if is_osc_query: + a_record.data = { "address": osc_query_server.http_server.bind_address } + else: + a_record.data = { "address": osc_query_server.osc_server_ip } + + var ptr_record = DNSRecord.new() + ptr_record.dns_type = DNSRecord.RECORD_TYPE.PTR + ptr_record.data = { "domain_labels": full_name } + ptr_record.labels = question.labels + + var answers : Array[DNSRecord] = [ptr_record] + var additional : Array[DNSRecord] = [txt_record, srv_record, a_record] + + var new_packet = DNSPacket.new() + new_packet.dns_answers = answers + new_packet.dns_additional = additional + new_packet.query_response = true + new_packet.conflict = true + new_packet.tentative = false + new_packet.truncation = false + new_packet.opcode = 0 + new_packet.response_code = 0 + new_packet.id = 0 + + # Send it off to our peers to alert them to the answer. + dns_service.send_packet(new_packet) + +func _vrc_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: if not packet.query_response: return if len(packet.dns_answers) == 0 or len(packet.dns_additional) == 0: @@ -544,9 +344,10 @@ func _dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: # Init osc sender. Default to 9000 (default OSC port). osc_client.change_port_and_ip(9000, ip_address) osc_client.start_client() - + # If it is the first time going through, we get the current avi params. + _get_avatar_params() + print("[VRChat OSC] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) - _get_avatar_params() func _get_avatar_params(): if vrchat_osc_query_endpoint == "": @@ -566,6 +367,12 @@ func _avatar_params_request_complete(result : int, response_code : int, print("[VRChat OSC] Avatar param request complete.") var json = JSON.parse_string(body.get_string_from_utf8()) + var root_contents : Dictionary = json["CONTENTS"] + if not root_contents.has("parameters") or not root_contents.has("change"): + # Could be booting game/loading/logging in/not in game. + printerr("[VRChat OSC] No parameters, or avatar information exists.") + return + # Uh oh... that's a lot of hardcoded values. # FIXME: Check that all these keys exist. current_avatar_id = json["CONTENTS"]["change"]["VALUE"][0] diff --git a/Mods/VRChatOSC/VRChatOSC.tscn b/Mods/VRChatOSC/VRChatOSC.tscn index 6ff32be..633f33d 100644 --- a/Mods/VRChatOSC/VRChatOSC.tscn +++ b/Mods/VRChatOSC/VRChatOSC.tscn @@ -1,13 +1,17 @@ -[gd_scene load_steps=4 format=3 uid="uid://cpe3ulnjnrapo"] +[gd_scene load_steps=6 format=3 uid="uid://cpe3ulnjnrapo"] [ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatOSC.gd" id="1_x840n"] [ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_b4fua"] [ext_resource type="Script" uid="uid://goefhxca5k8g" path="res://Mods/VMCController/KiriOSC/KiriOSCClient.gd" id="3_faft7"] +[ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/OSCQueryServer.gd" id="4_ovfdg"] +[ext_resource type="Script" uid="uid://csrri2vhxv4w5" path="res://Mods/VMCController/KiriOSC/KiriOSCServer.gd" id="5_lpbnq"] -[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service", "osc_client")] +[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] script = ExtResource("1_x840n") dns_service = NodePath("MulticastDNS") +update_vrc_param_values = null osc_client = NodePath("KiriOSClient") +osc_query_server = NodePath("OSCQueryServer") metadata/_custom_type_script = "uid://bsjotf2wtnmeq" [node name="MulticastDNS" type="Node" parent="."] @@ -17,3 +21,12 @@ metadata/_custom_type_script = "uid://bfemeu7ysbxwc" [node name="KiriOSClient" type="Node" parent="."] script = ExtResource("3_faft7") metadata/_custom_type_script = "uid://goefhxca5k8g" + +[node name="OSCQueryServer" type="Node" parent="." node_paths=PackedStringArray("osc_server")] +script = ExtResource("4_ovfdg") +osc_server = NodePath("../KiriOSCServer") +metadata/_custom_type_script = "uid://bg1sitssmsggs" + +[node name="KiriOSCServer" type="Node" parent="."] +script = ExtResource("5_lpbnq") +metadata/_custom_type_script = "uid://csrri2vhxv4w5" diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index 48330ff..b9c6cbc 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -62,8 +62,8 @@ func to_packet(packet: StreamPeerBuffer, cache: Dictionary) -> void: RECORD_TYPE.SRV: # priority, weight, port, target - packet.put_u16(data["priority"]) - packet.put_u16(data["weight"]) + packet.put_u16(data.get("priority", 1)) + packet.put_u16(data.get("weight", 1)) packet.put_u16(data["port"]) # target is a domain name (labels array) var srv_q = DNSQuestion.new() diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index 6fb8fd1..2da94d4 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -51,7 +51,8 @@ func _process(delta : float) -> void: on_receive.emit(dns_packet, packet) -func send_query(packet : DNSPacket): +## Sends DNS Packet to all connected client peers (UDP). +func send_packet(packet : DNSPacket): var raw_packet : StreamPeerBuffer = packet.to_packet() var byte_array : PackedByteArray = raw_packet.data_array for client : PacketPeerUDP in clients: diff --git a/Mods/VRChatOSC/godot-multicast-dns/README.md b/Mods/VRChatOSC/godot-multicast-dns/README.md index 6e411fc..ca7156a 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/README.md +++ b/Mods/VRChatOSC/godot-multicast-dns/README.md @@ -1,5 +1,5 @@ # Description -This library implements Multicast DNS (MDNS) functionality, primarily receiving MDNS responses. +This library implements DNS and Multicast DNS (MDNS) functionality, primarily receiving MDNS responses. # Features Interprets query responses and allows easy access, allows for sending DNS Packets in response. diff --git a/Mods/VRChatOSC/godottpd/http_response.gd b/Mods/VRChatOSC/godottpd/http_response.gd index 32f8e73..72a457c 100644 --- a/Mods/VRChatOSC/godottpd/http_response.gd +++ b/Mods/VRChatOSC/godottpd/http_response.gd @@ -10,7 +10,7 @@ var client: StreamPeer var server_identifier: String = "GodotTPD" ## A dictionary of headers -## [br] Headers can be set using the `set(name, value)` function +## [br] Headers can be set using the `set_header(name, value)` function var headers: Dictionary = {} ## An array of cookies @@ -77,7 +77,7 @@ func json(status_code: int, data) -> void: ## [br] ## [br][param field] - The name of the header. i.e. [code]Accept-Type[/code] ## [br][param value] - The value of this header. i.e. [code]application/json[/code] -func set(field: StringName, value: Variant) -> void: +func set_header(field: StringName, value: Variant) -> void: headers[field] = value From e35492a9097f3eca46f79828799ea339b8c29da1 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 27 Sep 2025 22:11:50 +0930 Subject: [PATCH 12/23] Add all possible shape keys Now to refine it down to those that are useful. --- Mods/VRChatOSC/VRCFTParameters.gd | 2024 +++++++++++++++++++++++++++-- Mods/VRChatOSC/VRChatOSC.gd | 82 +- Mods/VRChatOSC/VRChatOSC.tscn | 4 +- 3 files changed, 1971 insertions(+), 139 deletions(-) diff --git a/Mods/VRChatOSC/VRCFTParameters.gd b/Mods/VRChatOSC/VRCFTParameters.gd index 0e36ae0..4d9b5e0 100644 --- a/Mods/VRChatOSC/VRCFTParameters.gd +++ b/Mods/VRChatOSC/VRCFTParameters.gd @@ -5,7 +5,12 @@ enum COMBINATION_TYPE { RANGE = 1, COPY = 2, AVERAGE = 3, - WEIGHTED = 4 + WEIGHTED = 4, + RANGE_AVERAGE = 5, + MAX = 6, + MIN = 7, + SUBTRACT = 8, + WEIGHTED_ADD = 9 } enum SHAPE_KEY_TYPE { MEDIAPIPE = 1, @@ -17,284 +22,2035 @@ enum DIRECTION { } static var simplified_parameter_mapping : Dictionary = { - "BrowUpRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "EyeWide": { + "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ { - "shape": "BrowOuterUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 + "shape": "EyeWideLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowInnerUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, + "shape": "EyeWideRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "BrowUpLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "EyeSquint": { + "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ { - "shape": "BrowOuterUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 + "shape": "EyeSquintLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowInnerUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, + "shape": "EyeSquintRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "BrowDownRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "EyesSquint": { + "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ { - "shape": "BrowLowererRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 + "shape": "EyeSquintLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowPinchRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 - }, + "shape": "EyeSquintRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "BrowDownLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "BrowUp": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "BrowLowererLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 + "shape": "BrowUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowPinchLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 + "shape": "BrowUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "BrowDown": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "BrowDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "BrowDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "MouthSmileRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "BrowInnerUp": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "MouthCornerPullRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 + "shape": "BrowInnerUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "MouthCornerSlantRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 + "shape": "BrowInnerUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "BrowOuterUp": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "BrowOuterUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "BrowOuterUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "MouthSmileLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, + "BrowExpressionRight": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ { - "shape": "MouthCornerPullLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 + "use_max_value": false, + "positive": [ + { + "shape": "BrowInnerUpRight" + }, + { + "shape": "BrowOuterUpRight" + } + ], + "negative": [ + { + "shape": "BrowDownRight" + } + ] + } + ] + }, + "BrowExpressionLeft": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ + { + "shape": "BrowInnerUpLeft" + }, + { + "shape": "BrowOuterUpLeft" + } + ], + "negative": [ + { + "shape": "BrowDownLeft" + } + ] + } + ] + }, + "BrowExpression": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "BrowExpressionRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "MouthCornerSlantLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 + "shape": "BrowExpressionLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "CheekSquint": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "CheekSquintLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "CheekSquintRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "MouthX": { + "CheekPuffSuckLeft": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "MouthRight", + "shape": "CheekPuffLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.POSITIVE }, { - "shape": "MouthLeft", + "shape": "CheekSuckLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE - }, + } ] }, - "JawX": { + "CheekPuffSuckRight": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "JawRight", + "shape": "CheekPuffRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.POSITIVE }, { - "shape": "JawLeft", + "shape": "CheekSuckRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE + } + ] + }, + "CheekPuffSuck": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ + { + "shape": "CheekPuffRight" + }, + { + "shape": "CheekPuffLeft" + } + ], + "negative": [ + { + "shape": "CheekSuckRight" + }, + { + "shape": "CheekSuckLeft" + } + ] + } + ] + }, + "CheekSuck": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "CheekSuckLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "CheekSuckRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "JawZ": { + "MouthUpperX": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "JawForward", + "shape": "MouthUpperRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.POSITIVE }, { - "shape": "JawBackward", + "shape": "MouthUpperLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE - }, + } ] }, - "EyeLidRight": { - "combination_type": COMBINATION_TYPE.COPY, + "MouthLowerX": { + "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "EyeClosedRight", + "shape": "MouthLowerRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "inverse": true + "direction": DIRECTION.POSITIVE }, { - "shape": "EyeLidRight", + "shape": "MouthLowerLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, + "direction": DIRECTION.NEGATIVE + } ] }, - "EyeLidLeft": { - "combination_type": COMBINATION_TYPE.COPY, + "LipSuckUpper": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeClosedLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "inverse": true + "shape": "LipSuckUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLidLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, + "shape": "LipSuckUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeLid": { + "LipSuckLower": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeLidLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "shape": "LipSuckLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLidRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, + "shape": "LipSuckLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeRightX": { - "combination_type": COMBINATION_TYPE.RANGE, + "LipSuck": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeLookOutRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE + "shape": "LipSuckUpper", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLookInRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, + "shape": "LipSuckLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeRightY": { - "combination_type": COMBINATION_TYPE.RANGE, + "LipFunnelUpper": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeLookUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE + "shape": "LipFunnelUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLookDownRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, + "shape": "LipFunnelUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeLeftX": { - "combination_type": COMBINATION_TYPE.RANGE, + "LipFunnelLower": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeLookInLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE + "shape": "LipFunnelLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLookOutLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE + "shape": "LipFunnelLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipFunnel": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipFunnelUpper", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "LipFunnelLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeLeftY": { - "combination_type": COMBINATION_TYPE.RANGE, + "LipPuckerUpper": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeLookUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE + "shape": "LipPuckerUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLookDownLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE + "shape": "LipPuckerUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipPuckerLower": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipPuckerLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "LipPuckerLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeX": { + "LipPuckerRight": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeRightX", + "shape": "LipPuckerUpperRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLeftX", + "shape": "LipPuckerLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipPuckerLeft": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipPuckerUpperLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "LipPuckerLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, - "EyeY": { + "LipPucker": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "EyeRightY", + "shape": "LipPuckerLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "EyeLeftY", + "shape": "LipPuckerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipSuckFunnelUpper": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "LipSuckUpper", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "LipFunnelUpper", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, -} - -static var legacy_parameter_mapping : Dictionary = { - "JawOpenSuck": { - "combination_type": COMBINATION_TYPE.RANGE, + "LipSuckFunnelLower": { + "combination_type": COMBINATION_TYPE.SUBTRACT, "combination_shapes": [ { - "shape": "JawOpen", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE + "shape": "LipSuckLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "CheekSuck", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE + "shape": "LipFunnelLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipSuckFunnelLowerLeft": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "LipSuckLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "LipFunnelLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, + "LipSuckFunnelLowerRight": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "LipSuckLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipFunnelLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipSuckFunnelUpperLeft": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "LipSuckUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipFunnelUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "LipSuckFunnelUpperRight": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "LipSuckUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipFunnelUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthTightenerStretch": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "MouthTightener", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthStretch", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthTightenerStretchLeft": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "MouthTightenerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthStretchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthTightenerStretchRight": { + "combination_type": COMBINATION_TYPE.SUBTRACT, + "combination_shapes": [ + { + "shape": "MouthTightenerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthStretchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthCornerYLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthCornerSlantLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthFrownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "MouthCornerYRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthCornerSlantRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthFrownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "MouthCornerY": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthCornerYLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthCornerYRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "SmileFrownRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthFrownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "SmileFrownLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthFrownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "SmileFrown": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "SmileFrownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "SmileFrownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "SmileSadRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthSadRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "SmileSadLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthSadLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "SmileSad": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "SmileSadRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "SmileSadLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "TongueX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "TongueRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "TongueLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "TongueY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "TongueUp", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "TongueDown", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "TongueArchY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "TongueCurlUp", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "TongueBendDown", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "TongueShape": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "TongueFlat", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "TongueSquish", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + } + ] + }, + "MouthUpperUp": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthUpperUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.5 + }, + { + "shape": "MouthUpperUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.5 + }, + ] + }, + "MouthLowerDown": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthLowerDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.5 + }, + { + "shape": "MouthLowerDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.5 + }, + ] + }, + "BrowUpRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowUpLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowDownRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "BrowDownLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowLowererLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.75 + }, + { + "shape": "BrowPinchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthCornerPullLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.8 + }, + { + "shape": "MouthCornerSlantLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.2 + }, + ] + }, + "MouthOpen": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "MouthUpperUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + { + "shape": "MouthUpperUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + { + "shape": "MouthLowerDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + { + "shape": "MouthLowerDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.25 + }, + ] + }, + "MouthX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawZ": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawForward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawBackward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLidRight": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLidLeft": { + "combination_type": COMBINATION_TYPE.COPY, + "combination_shapes": [ + { + "shape": "EyeClosedLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "inverse": true + }, + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeLid": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeLidLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "EyeLidRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthStretch": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthStretchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthStretchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthTightener": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthTightenerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthTightenerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthPress": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthPressRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthPressLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthDimple": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthDimpleRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthDimpleLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "NoseSneer": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "NoseSneerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "NoseSneerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "EyeRightX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookOutRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookInRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeRightY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookInLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookOutLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeLeftY": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookDownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "EyeX": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftX", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, + "EyeY": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "EyeRightY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "EyeLeftY", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + ] + }, +} +# These are mostly taken from the mapper here: +# https://github.com/benaclejames/VRCFaceTracking/blob/a4a66fcd7ee776b1740512a481aecac686224af0/VRCFaceTracking.Core/Params/Expressions/Legacy/Lip/UnifiedSRanMapper.cs +static var legacy_parameter_mapping : Dictionary = { + "MouthApeShape": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "MouthClosed", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 1.0 + } + ] + }, + "MouthUpperOverturn": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipFunnelUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipFunnelUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthLowerOverturn": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipFunnelLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipFunnelLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthPout": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipPuckerUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipPuckerUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipPuckerLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipPuckerLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.MAX, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthDimpleRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.MAX, + "combination_shapes": [ + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthDimpleLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "CheekPuffLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "CheekPuffLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 1.0 + } + ] + }, + "CheekSuck": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "CheekSuckLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "CheekSuckRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthUpperInside": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipSuckUpperLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipSuckUpperRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthLowerInside": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "LipSuckLowerLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "LipSuckLowerRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthLowerOverlay": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "MouthRaiserLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 1.0 + } + ] + }, + "TongueLongStep1": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "TongueOut", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 2.0 + } + ] + }, + + # -------- START SIMPLIFIED LEGACY PARAMETERS -------- + # These are done at the end of the mapping for the + # sole purpose of simplification of legacy parameters. + # -------- START SIMPLIFIED LEGACY PARAMETERS -------- + "SmileSadRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthSadRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "SmileSadLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthSadLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "PuffSuckRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "CheekPuffRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekSuck", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "PuffSuckLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "CheekPuffLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekSuck", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "PuffSuck": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ + { + "shape": "CheekPuffLeft" + }, + { + "shape": "CheekPuffRight" + } + ], + "negative": [ + { + "shape": "CheekSuck" + } + ] + }, + ] + }, + "JawOpenSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekSuck", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthApeShape", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenPuffRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekPuffRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenPuffLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "CheekPuffLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenForward": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "JawForward", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "JawOpen", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthLowerOverlay", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, + "JawOpenPuff": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ + { + # Defaults to shape type unified. + "shape": "JawOpen" + } + ], + "negative": [ + { + "shape": "CheekPuffLeft" + }, + { + "shape": "CheekPuffRight" + } + ] + }, + ] + }, + "MouthUpperUpRightUpperInside": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "MouthUpperInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpRightPuffRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "CheekPuffRight", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpRightApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpRightPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpRightOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpRightSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpRight" }, + { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftUpperInside": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "MouthUpperInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftPuffLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "CheekPuffLeft", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpLeftSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperUpLeft" }, + { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthUpperUpUpperInside": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "MouthUpperInside" } ] + } + ] + }, + "MouthUpperUpInside": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "MouthUpperInside" }, { "shape": "MouthLowerInside" } ] + } + ] + }, + "MouthUpperUpPuff": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "CheekPuffLeft" }, { "shape": "CheekPuffRight" } ] + } + ] + }, + "MouthUpperUpPuffLeft": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "CheekPuffLeft" } ] + } + ] + }, + "MouthUpperUpPuffRight": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "CheekPuffRight" } ] + } + ] + }, + "MouthUpperUpApe": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "MouthApeShape" } ] + } + ] + }, + "MouthUpperUpPout": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "MouthPout" } ] + } + ] + }, + "MouthUpperUpOverlay": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "MouthLowerOverlay" } ] + } + ] + }, + "MouthUpperUpSuck": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], + "negative": [ { "shape": "CheekSuck" } ] + } + ] + }, + "MouthLowerDownRightLowerInside": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "MouthLowerInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownRightPuffRight": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "CheekPuffRight", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownRightApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownRightPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownRightOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownRightSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownRight" }, + { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftLowerInside": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "MouthLowerInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftPuffLeft": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "CheekPuffLeft", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLeftSuck": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerDownLeft" }, + { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerDownLowerInside": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "MouthLowerInside" } ] + } + ] + }, + "MouthLowerDownInside": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "MouthUpperInside" }, { "shape": "MouthLowerInside" } ] + } + ] + }, + "MouthLowerDownPuff": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "CheekPuffLeft" }, { "shape": "CheekPuffRight" } ] + } + ] + }, + "MouthLowerDownPuffLeft": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "CheekPuffLeft" } ] + } + ] + }, + "MouthLowerDownPuffRight": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "CheekPuffRight" } ] + } + ] + }, + "MouthLowerDownApe": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "MouthApeShape" } ] + } + ] + }, + "MouthLowerDownPout": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "MouthPout" } ] + } + ] + }, + "MouthLowerDownOverlay": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "MouthLowerOverlay" } ] + } + ] + }, + "MouthLowerDownSuck": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], + "negative": [ { "shape": "CheekSuck" } ] + } + ] + }, + "MouthUpperInsideOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthUpperInside" }, + { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "MouthLowerInsideOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthLowerInside" }, + { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileRightUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileRight" }, + { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileRightLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileRight" }, + { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileRightOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "SmileRightApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileRight" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileRightOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileRight" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileRightPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileRight" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileLeftUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileLeft" }, + { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileLeftLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileLeft" }, + { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileLeftOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "SmileLeftApe": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileLeft" }, + { "shape": "MouthApeShape", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileLeftOverlay": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileLeft" }, + { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileLeftPout": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "MouthSmileLeft" }, + { "shape": "MouthPout", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "SmileUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthUpperOverturn" } ] + } + ] + }, + "SmileLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "SmileOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "SmileApe": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthApeShape" } ] + } + ] + }, + "SmileOverlay": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthLowerOverlay" } ] + } + ] + }, + "SmilePout": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "MouthSmileLeft" }, { "shape": "MouthSmileRight" } ], + "negative": [ { "shape": "MouthPout" } ] + } + ] + }, + "PuffRightUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "CheekPuffRight" }, + { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "PuffRightLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "CheekPuffRight" }, + { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "PuffRightOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ { "shape": "CheekPuffRight" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "PuffLeftUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "CheekPuffLeft" }, + { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "PuffLeftLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { "shape": "CheekPuffLeft" }, + { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } + ] + }, + "PuffLeftOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ { "shape": "CheekPuffLeft" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "PuffUpperOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], + "negative": [ { "shape": "MouthUpperOverturn" } ] + } + ] + }, + "PuffLowerOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": false, + "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], + "negative": [ { "shape": "MouthLowerOverturn" } ] + } + ] + }, + "PuffOverturn": { + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, + "combination_shapes": [ + { + "use_max_value": true, + "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], + "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] + } + ] + } } diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index e104eab..eaedaf6 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -17,7 +17,7 @@ var vrc_params : VRCParams = VRCParams.new() ## Keys for quick lookup and verification. var vrc_param_keys : Array[String] = [] var avatar_req : HTTPRequest -var client_send_rate_limit_ms : int = 500 +var client_send_rate_limit_ms : int = 50 var curr_client_send_time : float var processing_request : bool = false # Can we not JUST USE THE SAME MAPPING @@ -128,6 +128,41 @@ func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictio sum += value count += 1 unified_blendshapes[param_name] = sum / max(count, 1) + + ParameterMappings.COMBINATION_TYPE.RANGE_AVERAGE: + var positive_shapes : Array = shapes[0]["positive"] + var negative_shapes : Array = shapes[0]["negative"] + var use_max_value : bool = shapes[0].get("use_max_value", false) + if use_max_value: + var max_pos : float = 0.0 + var max_neg : float = 0.0 + for shape_info : Dictionary in positive_shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + if value > max_pos: + max_pos = value + for shape_info : Dictionary in negative_shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + if value > max_neg: + max_neg = value + unified_blendshapes[param_name] = max_pos + (max_neg * -1.0) + else: + var sum_pos : float = 0.0 + var sum_neg : float = 0.0 + for shape_info : Dictionary in positive_shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + sum_pos += value + for shape_info : Dictionary in negative_shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + sum_neg += value + unified_blendshapes[param_name] = (sum_pos / max(len(positive_shapes), 1)) + ((sum_neg * -1.0) / max(len(negative_shapes), 1)) ParameterMappings.COMBINATION_TYPE.RANGE: var total : float = 0.0 @@ -141,7 +176,17 @@ func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictio else: total -= value unified_blendshapes[param_name] = total - + + ParameterMappings.COMBINATION_TYPE.WEIGHTED_ADD: + var total : float = 0.0 + for shape_info : Dictionary in shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var weight : float = shape_info.get("weight", 1.0) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + total += value * weight + unified_blendshapes[param_name] = total + ParameterMappings.COMBINATION_TYPE.WEIGHTED: var src_shape_info : Dictionary = shapes[0] var src_shape : String = src_shape_info["shape"] @@ -156,6 +201,39 @@ func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictio var dst_weight : float = dst_shape_info["weight"] unified_blendshapes[param_name] = src_value * src_weight + dst_value * dst_weight + + ParameterMappings.COMBINATION_TYPE.SUBTRACT: + var src_shape_info : Dictionary = shapes[0] + var src_shape : String = src_shape_info["shape"] + var src_type : ParameterMappings.SHAPE_KEY_TYPE = src_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var src_value : float = _get_unified_value(src_shape, src_type, unified_blendshapes) + + var dst_shape_info : Dictionary = shapes[1] + var dst_shape : String = dst_shape_info["shape"] + var dst_type : ParameterMappings.SHAPE_KEY_TYPE = dst_shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var dst_value : float = _get_unified_value(dst_shape, dst_type, unified_blendshapes) + + unified_blendshapes[param_name] = src_value - dst_value + + ParameterMappings.COMBINATION_TYPE.MAX: + var max_pos : float = 0.0 + for shape_info : Dictionary in shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + if value > max_pos: + max_pos = value + unified_blendshapes[param_name] = max_pos + + ParameterMappings.COMBINATION_TYPE.MIN: + var min : float = 1.1 # Very unlikely > 1.0 exists given they're constrainted to 1.0 + for shape_info : Dictionary in shapes: + var shape : String = shape_info["shape"] + var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) + var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) + if value < min: + min = value + unified_blendshapes[param_name] = min func _ready() -> void: avatar_req = HTTPRequest.new() diff --git a/Mods/VRChatOSC/VRChatOSC.tscn b/Mods/VRChatOSC/VRChatOSC.tscn index 633f33d..6dd90e0 100644 --- a/Mods/VRChatOSC/VRChatOSC.tscn +++ b/Mods/VRChatOSC/VRChatOSC.tscn @@ -6,13 +6,11 @@ [ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/OSCQueryServer.gd" id="4_ovfdg"] [ext_resource type="Script" uid="uid://csrri2vhxv4w5" path="res://Mods/VMCController/KiriOSC/KiriOSCServer.gd" id="5_lpbnq"] -[node name="VrChatOsc" type="Node" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] +[node name="VrChatOsc" type="Node3D" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] script = ExtResource("1_x840n") dns_service = NodePath("MulticastDNS") -update_vrc_param_values = null osc_client = NodePath("KiriOSClient") osc_query_server = NodePath("OSCQueryServer") -metadata/_custom_type_script = "uid://bsjotf2wtnmeq" [node name="MulticastDNS" type="Node" parent="."] script = ExtResource("2_b4fua") From eac406a81544be55bc633a971e9a1a6a64aff24d Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 27 Sep 2025 23:20:44 +0930 Subject: [PATCH 13/23] Adjust parameters, start caching --- Mods/VRChatOSC/VRCFTParameters.gd | 856 ++---------------------------- Mods/VRChatOSC/VRCParams.gd | 10 + Mods/VRChatOSC/VRChatOSC.gd | 43 +- 3 files changed, 85 insertions(+), 824 deletions(-) diff --git a/Mods/VRChatOSC/VRCFTParameters.gd b/Mods/VRChatOSC/VRCFTParameters.gd index 4d9b5e0..b803821 100644 --- a/Mods/VRChatOSC/VRCFTParameters.gd +++ b/Mods/VRChatOSC/VRCFTParameters.gd @@ -181,73 +181,6 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, - "CheekPuffSuckLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "CheekPuffLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekSuckLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "CheekPuffSuckRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "CheekPuffRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekSuckRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "CheekPuffSuck": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ - { - "shape": "CheekPuffRight" - }, - { - "shape": "CheekPuffLeft" - } - ], - "negative": [ - { - "shape": "CheekSuckRight" - }, - { - "shape": "CheekSuckLeft" - } - ] - } - ] - }, - "CheekSuck": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "CheekSuckLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "CheekSuckRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, "MouthUpperX": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -263,281 +196,6 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, - "MouthLowerX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "LipSuckUpper": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipSuckUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipSuckUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckLower": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipSuckLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipSuckLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuck": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipSuckUpper", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipSuckLower", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipFunnelUpper": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipFunnelUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipFunnelLower": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipFunnelLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipFunnel": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipFunnelUpper", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLower", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipPuckerUpper": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipPuckerUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipPuckerLower": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipPuckerLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipPuckerRight": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipPuckerUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipPuckerLeft": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipPuckerUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipPucker": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipPuckerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelUpper": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckUpper", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelUpper", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelLower": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckLower", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLower", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelLowerLeft": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelLowerRight": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelUpperLeft": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "LipSuckFunnelUpperRight": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "LipSuckUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthTightenerStretch": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "MouthTightener", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "MouthStretch", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthTightenerStretchLeft": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "MouthTightenerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "MouthStretchLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthTightenerStretchRight": { - "combination_type": COMBINATION_TYPE.SUBTRACT, - "combination_shapes": [ - { - "shape": "MouthTightenerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "MouthStretchRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, "MouthCornerYLeft": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -667,66 +325,6 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, - "TongueX": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "TongueRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "TongueLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "TongueY": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "TongueUp", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "TongueDown", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "TongueArchY": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "TongueCurlUp", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "TongueBendDown", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "TongueShape": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "TongueFlat", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "TongueSquish", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, "MouthUpperUp": { "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ @@ -971,19 +569,6 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, - "MouthTightener": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "MouthTightenerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - { - "shape": "MouthTightenerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - ] - }, "MouthPress": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ @@ -1038,6 +623,21 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, + "RightEyeX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookOutRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookInRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, "EyeRightY": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1068,6 +668,21 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, + "LeftEyeX": { + "combination_type": COMBINATION_TYPE.RANGE, + "combination_shapes": [ + { + "shape": "EyeLookInLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE + }, + { + "shape": "EyeLookOutLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE + }, + ] + }, "EyeLeftY": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1109,138 +724,55 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, -} -# These are mostly taken from the mapper here: -# https://github.com/benaclejames/VRCFaceTracking/blob/a4a66fcd7ee776b1740512a481aecac686224af0/VRCFaceTracking.Core/Params/Expressions/Legacy/Lip/UnifiedSRanMapper.cs -static var legacy_parameter_mapping : Dictionary = { - "MouthApeShape": { - "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, - "combination_shapes": [ - { - "shape": "MouthClosed", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 1.0 - } - ] - }, - "MouthUpperOverturn": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipFunnelUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthLowerOverturn": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "LipFunnelLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipFunnelLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthPout": { + "EyesY": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "LipPuckerUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerLowerLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "LipPuckerLowerRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthSmileRight": { - "combination_type": COMBINATION_TYPE.MAX, - "combination_shapes": [ - { - "shape": "MouthSmileRight", + "shape": "EyeRightY", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "MouthDimpleRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthSmileLeft": { - "combination_type": COMBINATION_TYPE.MAX, - "combination_shapes": [ - { - "shape": "MouthSmileLeft", + "shape": "EyeLeftY", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, - { - "shape": "MouthDimpleLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } ] }, - "CheekPuffLeft": { +} +# These are mostly taken from the mapper here: +# https://github.com/benaclejames/VRCFaceTracking/blob/a4a66fcd7ee776b1740512a481aecac686224af0/VRCFaceTracking.Core/Params/Expressions/Legacy/Lip/UnifiedSRanMapper.cs +static var legacy_parameter_mapping : Dictionary = { + "MouthApeShape": { "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, "combination_shapes": [ { - "shape": "CheekPuffLeft", + "shape": "MouthClosed", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "weight": 1.0 } ] }, - "CheekSuck": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "CheekSuckLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "CheekSuckRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "MouthUpperInside": { - "combination_type": COMBINATION_TYPE.AVERAGE, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ { - "shape": "LipSuckUpperLeft", + "shape": "MouthSmileRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "LipSuckUpperRight", + "shape": "MouthDimpleRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED } ] }, - "MouthLowerInside": { - "combination_type": COMBINATION_TYPE.AVERAGE, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ { - "shape": "LipSuckLowerLeft", + "shape": "MouthSmileLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "LipSuckLowerRight", + "shape": "MouthDimpleLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED } ] @@ -1255,16 +787,6 @@ static var legacy_parameter_mapping : Dictionary = { } ] }, - "TongueLongStep1": { - "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, - "combination_shapes": [ - { - "shape": "TongueOut", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 2.0 - } - ] - }, # -------- START SIMPLIFIED LEGACY PARAMETERS -------- # These are done at the end of the mapping for the @@ -1300,69 +822,16 @@ static var legacy_parameter_mapping : Dictionary = { }, ] }, - "PuffSuckRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "CheekPuffRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekSuck", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "PuffSuckLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "CheekPuffLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekSuck", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "PuffSuck": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": true, - "positive": [ - { - "shape": "CheekPuffLeft" - }, - { - "shape": "CheekPuffRight" - } - ], - "negative": [ - { - "shape": "CheekSuck" - } - ] - }, - ] - }, "JawOpenSuck": { - "combination_type": COMBINATION_TYPE.RANGE, + "combination_type": COMBINATION_TYPE.COPY, "combination_shapes": [ { "shape": "JawOpen", "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE }, { - "shape": "CheekSuck", + "shape": "JawOpenSuck", "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE }, ] }, @@ -1381,36 +850,6 @@ static var legacy_parameter_mapping : Dictionary = { }, ] }, - "JawOpenPuffRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "JawOpen", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekPuffRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, - "JawOpenPuffLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "JawOpen", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "CheekPuffLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, - ] - }, "JawOpenForward": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1498,13 +937,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthUpperUpRightSuck": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthUpperUpRight" }, - { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthUpperUpLeftUpperInside": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1512,13 +944,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthUpperInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthUpperUpLeftPuffLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthUpperUpLeft" }, - { "shape": "CheekPuffLeft", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthUpperUpLeftApe": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1540,13 +965,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthUpperUpLeftSuck": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthUpperUpLeft" }, - { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthUpperUpUpperInside": { "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ @@ -1567,36 +985,6 @@ static var legacy_parameter_mapping : Dictionary = { } ] }, - "MouthUpperUpPuff": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], - "negative": [ { "shape": "CheekPuffLeft" }, { "shape": "CheekPuffRight" } ] - } - ] - }, - "MouthUpperUpPuffLeft": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], - "negative": [ { "shape": "CheekPuffLeft" } ] - } - ] - }, - "MouthUpperUpPuffRight": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthUpperUpLeft" }, { "shape": "MouthUpperUpRight" } ], - "negative": [ { "shape": "CheekPuffRight" } ] - } - ] - }, "MouthUpperUpApe": { "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ @@ -1644,13 +1032,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthLowerDownRightPuffRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthLowerDownRight" }, - { "shape": "CheekPuffRight", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthLowerDownRightApe": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1672,13 +1053,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthLowerDownRightSuck": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthLowerDownRight" }, - { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthLowerDownLeftLowerInside": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1686,13 +1060,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerInside", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthLowerDownLeftPuffLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthLowerDownLeft" }, - { "shape": "CheekPuffLeft", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthLowerDownLeftApe": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1714,13 +1081,6 @@ static var legacy_parameter_mapping : Dictionary = { { "shape": "MouthLowerOverlay", "direction": ParameterMappings.DIRECTION.NEGATIVE } ] }, - "MouthLowerDownLeftSuck": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "MouthLowerDownLeft" }, - { "shape": "CheekSuck", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, "MouthLowerDownLowerInside": { "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ @@ -1741,36 +1101,6 @@ static var legacy_parameter_mapping : Dictionary = { } ] }, - "MouthLowerDownPuff": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], - "negative": [ { "shape": "CheekPuffLeft" }, { "shape": "CheekPuffRight" } ] - } - ] - }, - "MouthLowerDownPuffLeft": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], - "negative": [ { "shape": "CheekPuffLeft" } ] - } - ] - }, - "MouthLowerDownPuffRight": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], - "negative": [ { "shape": "CheekPuffRight" } ] - } - ] - }, "MouthLowerDownApe": { "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ @@ -1801,16 +1131,6 @@ static var legacy_parameter_mapping : Dictionary = { } ] }, - "MouthLowerDownSuck": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "MouthLowerDownLeft" }, { "shape": "MouthLowerDownRight" } ], - "negative": [ { "shape": "CheekSuck" } ] - } - ] - }, "MouthUpperInsideOverturn": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -1975,82 +1295,4 @@ static var legacy_parameter_mapping : Dictionary = { } ] }, - "PuffRightUpperOverturn": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "CheekPuffRight" }, - { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, - "PuffRightLowerOverturn": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "CheekPuffRight" }, - { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, - "PuffRightOverturn": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": true, - "positive": [ { "shape": "CheekPuffRight" } ], - "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] - } - ] - }, - "PuffLeftUpperOverturn": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "CheekPuffLeft" }, - { "shape": "MouthUpperOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, - "PuffLeftLowerOverturn": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { "shape": "CheekPuffLeft" }, - { "shape": "MouthLowerOverturn", "direction": ParameterMappings.DIRECTION.NEGATIVE } - ] - }, - "PuffLeftOverturn": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": true, - "positive": [ { "shape": "CheekPuffLeft" } ], - "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] - } - ] - }, - "PuffUpperOverturn": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], - "negative": [ { "shape": "MouthUpperOverturn" } ] - } - ] - }, - "PuffLowerOverturn": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": false, - "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], - "negative": [ { "shape": "MouthLowerOverturn" } ] - } - ] - }, - "PuffOverturn": { - "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, - "combination_shapes": [ - { - "use_max_value": true, - "positive": [ { "shape": "CheekPuffRight" }, { "shape": "CheekPuffLeft" } ], - "negative": [ { "shape": "MouthUpperOverturn" }, { "shape": "MouthLowerOverturn" } ] - } - ] - } } diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index 5c1c131..a4601d8 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -50,10 +50,20 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ param ) pass + +func valid_params_from_dict(dict : Dictionary) -> Array[String]: + var keys = dict.keys() + var valid = _params.filter(func (p : VRCParam): return p.binary_key in keys) + var shapes : Array[String] = [] + for valid_param in valid: + shapes.append(valid_param.binary_key) + return shapes ## Updates a particular key to the supplied value. ## This func takes care of the exchange between binary/float parameters in VRC tracking. func update_value(key : String, value): + + # TODO: Add cache for binary_key -> VRCParam. Make sure to reset in .reset method. var params : Array[VRCParam] = _params.filter(func (p : VRCParam): return p.binary_key == key) if len(params) == 0: return diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index eaedaf6..3f1847a 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -8,6 +8,8 @@ class_name VRChatOSC var osc_query_name : String = str(randi_range(500000, 5000000)) var osc_server_name : String = str(randi_range(500000, 5000000)) var vrchat_osc_query_endpoint : String = "" +## Cached processed keys that exist on the current avatar. +var cached_valid_keys : Array[String] = [] ## The current value of the avatar ID. var current_avatar_id : String ## The previous value of the avatar ID. @@ -263,25 +265,29 @@ func _process(delta : float) -> void: if vrchat_osc_query_endpoint == "": return - curr_client_send_time += delta - if curr_client_send_time > client_send_rate_limit_ms / 1000: - curr_client_send_time = 0 + #curr_client_send_time += delta + #if curr_client_send_time > client_send_rate_limit_ms / 1000: + # curr_client_send_time = 0 - # Map the blendshapes we have from mediapipe to the unified versions. - var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() - - # Apply unified blendshape simplification mapping - _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) - - # Apply legacy parameter mapping (this makes me sad) - _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) - - # Set params to values - for shape in unified_blendshapes: - vrc_params.update_value(shape, unified_blendshapes[shape]) + # Map the blendshapes we have from mediapipe to the unified versions. + var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() + + # Apply unified blendshape simplification mapping + _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) + # Apply legacy parameter mapping (this makes me sad) + _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) + + if len(cached_valid_keys) == 0: + cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) + + # Set params to values + for shape in unified_blendshapes: + if not shape in cached_valid_keys: + continue + vrc_params.update_value(shape, unified_blendshapes[shape]) - # Finally, send all dirty params off to VRC - _send_dirty_params() + # Finally, send all dirty params off to VRC + _send_dirty_params() func _map_blendshapes_to_unified() -> Dictionary: @@ -291,6 +297,8 @@ func _map_blendshapes_to_unified() -> Dictionary: if not arkit_to_unified_mapping.has(blendshape): continue var unified_blendshape = arkit_to_unified_mapping[blendshape] + #if len(cached_valid_keys) > 0 and not cached_valid_keys.has(unified_blendshape): + # continue unified_blendshapes[unified_blendshape] = blendshapes[blendshape] return unified_blendshapes @@ -459,6 +467,7 @@ func _avatar_params_request_complete(result : int, response_code : int, # Update only if changed avi. print("[VRChat OSC] Avatar has changed. Updating parameter keys, values and types.") vrc_param_keys = [] + cached_valid_keys = [] vrc_params.reset() # We always pull raw avatar params to update the current value. From be922cf1c34df33d63bf5555c0c9dc232abd3622 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sun, 28 Sep 2025 18:53:58 +0930 Subject: [PATCH 14/23] Further tweaking of shape transforms --- Mods/VMCController/KiriOSC/KiriOSCClient.gd | 2 +- Mods/VRChatOSC/VRCFTParameters.gd | 453 +++++++------------- Mods/VRChatOSC/VRCParams.gd | 6 +- Mods/VRChatOSC/VRChatOSC.gd | 85 ++-- 4 files changed, 231 insertions(+), 315 deletions(-) diff --git a/Mods/VMCController/KiriOSC/KiriOSCClient.gd b/Mods/VMCController/KiriOSC/KiriOSCClient.gd index 220c1fe..bfa3928 100644 --- a/Mods/VMCController/KiriOSC/KiriOSCClient.gd +++ b/Mods/VMCController/KiriOSC/KiriOSCClient.gd @@ -153,7 +153,7 @@ func _osc_string(s : String) -> PackedByteArray: # Technically the OSC specs state "non-null ASCII characters followed by null", but we use utf8. # This is due to the comm format in VMC: "Use UTF-8. (Data includes non ascii type)" - packet.append_array(s.to_utf8_buffer()) + packet.append_array(s.to_ascii_buffer()) packet.append(0) # Pad to a multiple of 4 bytes diff --git a/Mods/VRChatOSC/VRCFTParameters.gd b/Mods/VRChatOSC/VRCFTParameters.gd index b803821..3543009 100644 --- a/Mods/VRChatOSC/VRCFTParameters.gd +++ b/Mods/VRChatOSC/VRCFTParameters.gd @@ -22,6 +22,45 @@ enum DIRECTION { } static var simplified_parameter_mapping : Dictionary = { + "MouthFrown": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthFrownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthFrownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthSmile": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, + "MouthStretch": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "MouthStretchRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + { + "shape": "MouthStretchLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + }, + ] + }, "EyeWide": { "combination_type": COMBINATION_TYPE.MAX, "combination_shapes": [ @@ -61,41 +100,58 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, - "BrowUp": { - "combination_type": COMBINATION_TYPE.AVERAGE, + "BrowUpRight": { + "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ { - "shape": "BrowUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED + "shape": "BrowOuterUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 }, { - "shape": "BrowUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } + "shape": "BrowInnerUpRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, ] }, - "BrowDown": { + "BrowUpLeft": { + "combination_type": COMBINATION_TYPE.WEIGHTED, + "combination_shapes": [ + { + "shape": "BrowOuterUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.6 + }, + { + "shape": "BrowInnerUpLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 0.4 + }, + ] + }, + "BrowUp": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "BrowDownRight", + "shape": "BrowUpRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowDownLeft", + "shape": "BrowUpLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED } ] }, - "BrowInnerUp": { + "BrowDown": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "BrowInnerUpLeft", + "shape": "BrowDownRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "BrowInnerUpRight", + "shape": "BrowDownLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED } ] @@ -185,146 +241,17 @@ static var simplified_parameter_mapping : Dictionary = { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "MouthUpperRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthUpperLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "MouthCornerYLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthCornerSlantLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthFrownLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "MouthCornerYRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthCornerSlantRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthFrownRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "MouthCornerY": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "MouthCornerYLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "MouthCornerYRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "SmileFrownRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthSmileRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthFrownRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "SmileFrownLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthSmileLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthFrownLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "SmileFrown": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "SmileFrownRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "SmileFrownLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, - "SmileSadRight": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthSmileRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthSadRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - } - ] - }, - "SmileSadLeft": { - "combination_type": COMBINATION_TYPE.RANGE, - "combination_shapes": [ - { - "shape": "MouthSmileLeft", + "shape": "MouthUpperUpRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.POSITIVE }, { - "shape": "MouthSadLeft", + "shape": "MouthUpperUpLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE } ] }, - "SmileSad": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "SmileSadRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - }, - { - "shape": "SmileSadLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED - } - ] - }, "MouthUpperUp": { "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ @@ -355,96 +282,6 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, - "BrowUpRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowOuterUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 - }, - { - "shape": "BrowInnerUpRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, - ] - }, - "BrowUpLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowOuterUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.6 - }, - { - "shape": "BrowInnerUpLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.4 - }, - ] - }, - "BrowDownRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowLowererRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 - }, - { - "shape": "BrowPinchRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 - }, - ] - }, - "BrowDownLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "BrowLowererLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.75 - }, - { - "shape": "BrowPinchLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.25 - }, - ] - }, - "MouthSmileRight": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "MouthCornerPullRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 - }, - { - "shape": "MouthCornerSlantRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 - }, - ] - }, - "MouthSmileLeft": { - "combination_type": COMBINATION_TYPE.WEIGHTED, - "combination_shapes": [ - { - "shape": "MouthCornerPullLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.8 - }, - { - "shape": "MouthCornerSlantLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 0.2 - }, - ] - }, "MouthOpen": { "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ @@ -471,18 +308,17 @@ static var simplified_parameter_mapping : Dictionary = { ] }, "MouthX": { - "combination_type": COMBINATION_TYPE.RANGE, + "combination_type": COMBINATION_TYPE.RANGE_AVERAGE, "combination_shapes": [ { - "shape": "MouthRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.POSITIVE - }, - { - "shape": "MouthLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "direction": DIRECTION.NEGATIVE - }, + "use_max_value": true, + "positive": [{ + "shape": "MouthUpperRight", + }], + "negative": [{ + "shape": "MouthLowerRight" + }] + } ] }, "JawX": { @@ -556,19 +392,6 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, - "MouthStretch": { - "combination_type": COMBINATION_TYPE.AVERAGE, - "combination_shapes": [ - { - "shape": "MouthStretchRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - { - "shape": "MouthStretchLeft", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - }, - ] - }, "MouthPress": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ @@ -741,57 +564,49 @@ static var simplified_parameter_mapping : Dictionary = { # These are mostly taken from the mapper here: # https://github.com/benaclejames/VRCFaceTracking/blob/a4a66fcd7ee776b1740512a481aecac686224af0/VRCFaceTracking.Core/Params/Expressions/Legacy/Lip/UnifiedSRanMapper.cs static var legacy_parameter_mapping : Dictionary = { - "MouthApeShape": { - "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "SmileFrownRight": { + "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "MouthClosed", + "shape": "MouthSmileRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 1.0 + "direction": DIRECTION.POSITIVE + }, + { + "shape": "MouthFrownRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE } ] }, - "MouthSmileRight": { - "combination_type": COMBINATION_TYPE.MAX, + "SmileFrownLeft": { + "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ { - "shape": "MouthSmileRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.POSITIVE }, { - "shape": "MouthDimpleRight", - "shape_type": SHAPE_KEY_TYPE.UNIFIED + "shape": "MouthFrownLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "direction": DIRECTION.NEGATIVE } ] }, - "MouthSmileLeft": { - "combination_type": COMBINATION_TYPE.MAX, + "SmileFrown": { + "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "MouthSmileLeft", + "shape": "SmileFrownRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED }, { - "shape": "MouthDimpleLeft", + "shape": "SmileFrownLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED } ] }, - "MouthLowerOverlay": { - "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, - "combination_shapes": [ - { - "shape": "MouthRaiserLower", - "shape_type": SHAPE_KEY_TYPE.UNIFIED, - "weight": 1.0 - } - ] - }, - - # -------- START SIMPLIFIED LEGACY PARAMETERS -------- - # These are done at the end of the mapping for the - # sole purpose of simplification of legacy parameters. - # -------- START SIMPLIFIED LEGACY PARAMETERS -------- "SmileSadRight": { "combination_type": COMBINATION_TYPE.RANGE, "combination_shapes": [ @@ -804,7 +619,7 @@ static var legacy_parameter_mapping : Dictionary = { "shape": "MouthSadRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE - }, + } ] }, "SmileSadLeft": { @@ -819,9 +634,73 @@ static var legacy_parameter_mapping : Dictionary = { "shape": "MouthSadLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, "direction": DIRECTION.NEGATIVE + } + ] + }, + "SmileSad": { + "combination_type": COMBINATION_TYPE.AVERAGE, + "combination_shapes": [ + { + "shape": "SmileSadRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED }, + { + "shape": "SmileSadLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } ] }, + "MouthApeShape": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "MouthClosed", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 1.0 + } + ] + }, + "MouthSmileRight": { + "combination_type": COMBINATION_TYPE.MAX, + "combination_shapes": [ + { + "shape": "MouthSmileRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthDimpleRight", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthSmileLeft": { + "combination_type": COMBINATION_TYPE.MAX, + "combination_shapes": [ + { + "shape": "MouthSmileLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + }, + { + "shape": "MouthDimpleLeft", + "shape_type": SHAPE_KEY_TYPE.UNIFIED + } + ] + }, + "MouthLowerOverlay": { + "combination_type": COMBINATION_TYPE.WEIGHTED_ADD, + "combination_shapes": [ + { + "shape": "MouthRaiserLower", + "shape_type": SHAPE_KEY_TYPE.UNIFIED, + "weight": 1.0 + } + ] + }, + + # -------- START SIMPLIFIED LEGACY PARAMETERS -------- + # These are done at the end of the mapping for the + # sole purpose of simplification of legacy parameters. + # -------- START SIMPLIFIED LEGACY PARAMETERS -------- "JawOpenSuck": { "combination_type": COMBINATION_TYPE.COPY, "combination_shapes": [ diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index a4601d8..3742b62 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -20,7 +20,11 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ _raw_params = raw_avatar_params _avatar_id = avatar_id _has_changed_avi = has_changed_avi - + + if raw_avatar_params.has("FT"): + raw_avatar_params = raw_avatar_params["FT"]["CONTENTS"] + _raw_params = raw_avatar_params + if raw_avatar_params.has("v2"): raw_avatar_params = raw_avatar_params["v2"]["CONTENTS"] _raw_params = raw_avatar_params diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index 3f1847a..0b6731d 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -19,7 +19,7 @@ var vrc_params : VRCParams = VRCParams.new() ## Keys for quick lookup and verification. var vrc_param_keys : Array[String] = [] var avatar_req : HTTPRequest -var client_send_rate_limit_ms : int = 50 +var client_send_rate_limit_ms : int = 500 var curr_client_send_time : float var processing_request : bool = false # Can we not JUST USE THE SAME MAPPING @@ -111,7 +111,8 @@ func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictio if src_value < 0: src_value = abs(src_value) else: - src_value *= -1 + # We inverse the value from 1.0 + src_value = 1.0 - src_value for i in range(1, shapes.size()): var dst_shape_info : Dictionary = shapes[i] @@ -260,34 +261,56 @@ func _ready() -> void: var new_value = key arkit_to_unified_mapping[new_key] = new_value +#FIXME: REMOVE BEFORE PR +var hits = {} func _process(delta : float) -> void: if vrchat_osc_query_endpoint == "": return - #curr_client_send_time += delta - #if curr_client_send_time > client_send_rate_limit_ms / 1000: - # curr_client_send_time = 0 + curr_client_send_time += delta + if curr_client_send_time > client_send_rate_limit_ms / 1000: + curr_client_send_time = 0 - # Map the blendshapes we have from mediapipe to the unified versions. - var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() - - # Apply unified blendshape simplification mapping - _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) - # Apply legacy parameter mapping (this makes me sad) - _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) + # Map the blendshapes we have from mediapipe to the unified versions. + var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() + + # Apply unified blendshape simplification mapping + _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) + + if unified_blendshapes.has("MouthStretchRight") and unified_blendshapes.has("MouthStretchLeft") \ + and unified_blendshapes.has("MouthSmileRight") and unified_blendshapes.has("MouthSmileLeft"): + # Set MouthSadLeft/Right - complexish conversions + unified_blendshapes["MouthSadRight"] = \ + maxf(0, \ + unified_blendshapes["MouthFrown"] > unified_blendshapes["MouthStretchRight"] + if unified_blendshapes["MouthFrown"] \ + else unified_blendshapes["MouthStretchRight"] - unified_blendshapes["MouthSmileRight"]) + unified_blendshapes["MouthSadLeft"] = \ + maxf(0, \ + unified_blendshapes["MouthFrown"] > unified_blendshapes["MouthStretchLeft"] + if unified_blendshapes["MouthFrown"] \ + else unified_blendshapes["MouthStretchLeft"] - unified_blendshapes["MouthSmileLeft"]) + + # Apply legacy parameter mapping (this makes me sad) + _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) - if len(cached_valid_keys) == 0: - cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) + if len(cached_valid_keys) == 0: + cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) - # Set params to values - for shape in unified_blendshapes: - if not shape in cached_valid_keys: - continue - vrc_params.update_value(shape, unified_blendshapes[shape]) + # Set params to values + for shape in unified_blendshapes: + if not hits.has(shape): + hits[shape] = 0 + if unified_blendshapes[shape] <= 0.001: + hits[shape] += 1 + if not shape in cached_valid_keys: + continue + vrc_params.update_value(shape, unified_blendshapes[shape]) - # Finally, send all dirty params off to VRC - _send_dirty_params() + # Finally, send all dirty params off to VRC + _send_dirty_params() + #print(unified_blendshapes["JawOpen"]) func _map_blendshapes_to_unified() -> Dictionary: @@ -305,8 +328,14 @@ func _map_blendshapes_to_unified() -> Dictionary: func _send_dirty_params(): var to_send_osc : Array[VRCParam] = vrc_params.get_dirty() - + var bundle : Array = [] + for param in to_send_osc: + if param.binary_key == "JawOpen": + #print("JawOpen") + #print(param.value) + #print(param.key) + pass param.reset_dirty() # We send the message with the full path for the avatar parameter, and type. var type = param.type @@ -316,8 +345,12 @@ func _send_dirty_params(): type = "T" else: type = "F" - osc_client.send_osc_message(param.full_path, type, [param.value]) - + bundle.append(osc_client.prepare_osc_message(param.full_path, type, [param.value])) + #osc_client.send_osc_message(param.full_path, type, [param.value]) + + var send = osc_client.create_osc_bundle(3535, bundle) + osc_client.send_osc_message_raw(send) + func _osc_query_received(address : String, args) -> void: if address == "/avatar/change": print("WAOH") @@ -447,6 +480,8 @@ func _get_avatar_params(): func _avatar_params_request_complete(result : int, response_code : int, headers: PackedStringArray, body: PackedByteArray) -> void: + processing_request = false + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: printerr("Request for VRC avatar params failed.") return @@ -473,8 +508,6 @@ func _avatar_params_request_complete(result : int, response_code : int, # We always pull raw avatar params to update the current value. var raw_avatar_params = json["CONTENTS"]["parameters"]["CONTENTS"] - processing_request = false - if not update_vrc_param_values and not has_changed_avi: previous_avatar_id = current_avatar_id return From e891e403c82281ca6f5a6c6c140d99aced66da9a Mon Sep 17 00:00:00 2001 From: Ellie Date: Fri, 3 Oct 2025 17:16:49 +0930 Subject: [PATCH 15/23] Add support for more legacy eye params --- Mods/VMCController/KiriOSC/KiriOSCClient.gd | 2 +- Mods/VRChatOSC/VRCFTParameters.gd | 9 +++-- Mods/VRChatOSC/VRChatOSC.gd | 35 ++++++++++++++++--- .../godot-multicast-dns/DNSRecord.gd | 2 +- .../godot-multicast-dns/MulticastDNS.gd | 3 +- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Mods/VMCController/KiriOSC/KiriOSCClient.gd b/Mods/VMCController/KiriOSC/KiriOSCClient.gd index bfa3928..220c1fe 100644 --- a/Mods/VMCController/KiriOSC/KiriOSCClient.gd +++ b/Mods/VMCController/KiriOSC/KiriOSCClient.gd @@ -153,7 +153,7 @@ func _osc_string(s : String) -> PackedByteArray: # Technically the OSC specs state "non-null ASCII characters followed by null", but we use utf8. # This is due to the comm format in VMC: "Use UTF-8. (Data includes non ascii type)" - packet.append_array(s.to_ascii_buffer()) + packet.append_array(s.to_utf8_buffer()) packet.append(0) # Pad to a multiple of 4 bytes diff --git a/Mods/VRChatOSC/VRCFTParameters.gd b/Mods/VRChatOSC/VRCFTParameters.gd index 3543009..b9ccb65 100644 --- a/Mods/VRChatOSC/VRCFTParameters.gd +++ b/Mods/VRChatOSC/VRCFTParameters.gd @@ -100,7 +100,7 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, - "BrowUpRight": { + "BrowUpRight": { "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ { @@ -224,6 +224,7 @@ static var simplified_parameter_mapping : Dictionary = { } ] }, + # Just group the already existing mediapipe keys. "CheekSquint": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ @@ -267,6 +268,7 @@ static var simplified_parameter_mapping : Dictionary = { }, ] }, + # Again, group from existing keys. "MouthLowerDown": { "combination_type": COMBINATION_TYPE.WEIGHTED, "combination_shapes": [ @@ -409,15 +411,16 @@ static var simplified_parameter_mapping : Dictionary = { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ { - "shape": "MouthDimpleRight", + "shape": "MouthDimplerRight", "shape_type": SHAPE_KEY_TYPE.UNIFIED, }, { - "shape": "MouthDimpleLeft", + "shape": "MouthDimplerLeft", "shape_type": SHAPE_KEY_TYPE.UNIFIED, }, ] }, + # Just group the existing mediapipe keys. "NoseSneer": { "combination_type": COMBINATION_TYPE.AVERAGE, "combination_shapes": [ diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index 0b6731d..e9e586d 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -291,7 +291,18 @@ func _process(delta : float) -> void: unified_blendshapes["MouthFrown"] > unified_blendshapes["MouthStretchLeft"] if unified_blendshapes["MouthFrown"] \ else unified_blendshapes["MouthStretchLeft"] - unified_blendshapes["MouthSmileLeft"]) - + + if unified_blendshapes.has("EyeWideLeft") \ + and unified_blendshapes.has("EyeWideRight") \ + and unified_blendshapes.has("EyeLidLeft") \ + and unified_blendshapes.has("EyeLidRight") \ + and unified_blendshapes.has("EyeSquintLeft") \ + and unified_blendshapes.has("EyeSquintRight"): + # Complex calcs separated out for simplicity. + # This ends up as Left/RightEyeLidExpandedSqueeze. + calc_eyelid_expanded_squeeze(unified_blendshapes, "Left", "EyeWideLeft", "EyeLidLeft", "EyeSquintLeft") + calc_eyelid_expanded_squeeze(unified_blendshapes, "Right", "EyeWideRight", "EyeLidRight", "EyeSquintRight") + # Apply legacy parameter mapping (this makes me sad) _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) @@ -307,12 +318,25 @@ func _process(delta : float) -> void: if not shape in cached_valid_keys: continue vrc_params.update_value(shape, unified_blendshapes[shape]) - # Finally, send all dirty params off to VRC _send_dirty_params() - #print(unified_blendshapes["JawOpen"]) - +func calc_squeeze(bs: Dictionary, wide_key: String, lid_key: String, squint_key: String) -> float: + var wide = bs[wide_key] + var lid = bs[lid_key] + var squint = bs[squint_key] + return wide * 0.2 + (lid * 0.8) - (1.0 - pow(lid, 0.15) * squint) + +func calc_eyelid_expanded_squeeze(bs: Dictionary, side: String, wide_key: String, lid_key: String, squint_key: String) -> void: + var value = calc_squeeze(bs, wide_key, lid_key, squint_key) + var target_key = side + "EyeLidExpandedSqueeze" + if value > 0.8: + bs[target_key] = bs[wide_key] + elif value >= 0.0: + bs[target_key] = bs[lid_key] + else: + bs[target_key] = (1.0 - pow(bs[lid_key], 0.15) * bs[squint_key]) + func _map_blendshapes_to_unified() -> Dictionary: var blendshapes : Dictionary = get_global_mod_data("BlendShapes") var unified_blendshapes : Dictionary = {} @@ -438,7 +462,8 @@ func _vrc_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: return var domain_label : String = ptr_record.data["full_label"] - if not domain_label.begins_with("VRChat-Client"): + if not domain_label.begins_with("VRChat-Client") \ + and not domain_label.begins_with("ChilloutVR-GameClient"): return var a_records : Array[DNSRecord] = packet.dns_additional.filter( diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index b9c6cbc..a9febaa 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -36,7 +36,7 @@ static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSReco elif dns_record.dns_type == RECORD_TYPE.TXT: dns_record._txt_record(packet) else: - print("Unsupported DNS record type found: %s", dns_record.dns_type) + print("Unsupported DNS record type found: ", dns_record.dns_type) return dns_record diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index 2da94d4..63ffb2f 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -20,7 +20,8 @@ func _process(delta : float) -> void: if server.is_connection_available(): var receiver = server.take_connection() for interface_details : Dictionary in IP.get_local_interfaces(): - receiver.join_multicast_group(multicast_address, interface_details["name"]) + if not receiver.is_bound(): + receiver.join_multicast_group(multicast_address, interface_details["name"]) # TODO: Make sender sockets for each local interface to support sending. for ip_addr in interface_details["addresses"]: if local_addresses.has(ip_addr): From 53f7e10d9059a3387b47b9ebf9d6c2b99bd52ae5 Mon Sep 17 00:00:00 2001 From: Ellie Date: Fri, 3 Oct 2025 18:19:52 +0930 Subject: [PATCH 16/23] Add fallback to root param list --- Mods/VRChatOSC/VRCParams.gd | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index 3742b62..9830672 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -21,6 +21,8 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ _avatar_id = avatar_id _has_changed_avi = has_changed_avi + var orig_params = raw_avatar_params + if raw_avatar_params.has("FT"): raw_avatar_params = raw_avatar_params["FT"]["CONTENTS"] _raw_params = raw_avatar_params @@ -28,9 +30,16 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ if raw_avatar_params.has("v2"): raw_avatar_params = raw_avatar_params["v2"]["CONTENTS"] _raw_params = raw_avatar_params - + # Only if we are wanting to update/change param values do we progress here. var param_names = raw_avatar_params.keys() + + # Catch-all fallback to the overall root avi parameters. + if len(param_names) == 0: + raw_avatar_params = orig_params + _raw_params = raw_avatar_params + param_names = raw_avatar_params.keys() + for key in param_names: # Verify this is a type/value parameter. if not "TYPE" in raw_avatar_params[key] or not "VALUE" in raw_avatar_params[key]: From a04e8aab33364233ca5235f3ce09536da1ffa77c Mon Sep 17 00:00:00 2001 From: Ellie Date: Fri, 3 Oct 2025 19:50:44 +0930 Subject: [PATCH 17/23] Add settings, start cleanup --- Mods/VRChatOSC/VRChatOSC.gd | 47 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatOSC.gd index e9e586d..6cb28ab 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatOSC.gd @@ -5,6 +5,9 @@ class_name VRChatOSC @export var update_vrc_param_values : bool = false @export var osc_client : KiriOSClient @export var osc_query_server : OSCQueryServer +# User Settings +var send_eye_tracking : bool = true +# Internal fields var osc_query_name : String = str(randi_range(500000, 5000000)) var osc_server_name : String = str(randi_range(500000, 5000000)) var vrchat_osc_query_endpoint : String = "" @@ -261,10 +264,20 @@ func _ready() -> void: var new_value = key arkit_to_unified_mapping[new_key] = new_value -#FIXME: REMOVE BEFORE PR -var hits = {} -func _process(delta : float) -> void: + add_tracked_setting("send_eye_tracking", "Send eye tracking blendshapes") + + var force_avatar_detection_button : Button = Button.new() + force_avatar_detection_button.text = "Force Avatar Refresh" + force_avatar_detection_button.pressed.connect( + func(): + previous_avatar_id = "" + _get_avatar_params() + update_settings_ui()) + get_settings_window().add_child(force_avatar_detection_button) + update_settings_ui() + +func _process(delta : float) -> void: if vrchat_osc_query_endpoint == "": return @@ -300,8 +313,8 @@ func _process(delta : float) -> void: and unified_blendshapes.has("EyeSquintRight"): # Complex calcs separated out for simplicity. # This ends up as Left/RightEyeLidExpandedSqueeze. - calc_eyelid_expanded_squeeze(unified_blendshapes, "Left", "EyeWideLeft", "EyeLidLeft", "EyeSquintLeft") - calc_eyelid_expanded_squeeze(unified_blendshapes, "Right", "EyeWideRight", "EyeLidRight", "EyeSquintRight") + _calc_eyelid_expanded_squeeze(unified_blendshapes, "Left", "EyeWideLeft", "EyeLidLeft", "EyeSquintLeft") + _calc_eyelid_expanded_squeeze(unified_blendshapes, "Right", "EyeWideRight", "EyeLidRight", "EyeSquintRight") # Apply legacy parameter mapping (this makes me sad) _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) @@ -310,25 +323,23 @@ func _process(delta : float) -> void: cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) # Set params to values - for shape in unified_blendshapes: - if not hits.has(shape): - hits[shape] = 0 - if unified_blendshapes[shape] <= 0.001: - hits[shape] += 1 + for shape : String in unified_blendshapes: if not shape in cached_valid_keys: continue + if not send_eye_tracking and shape.contains("Eye"): + continue vrc_params.update_value(shape, unified_blendshapes[shape]) # Finally, send all dirty params off to VRC _send_dirty_params() -func calc_squeeze(bs: Dictionary, wide_key: String, lid_key: String, squint_key: String) -> float: +func _calc_squeeze(bs: Dictionary, wide_key: String, lid_key: String, squint_key: String) -> float: var wide = bs[wide_key] var lid = bs[lid_key] var squint = bs[squint_key] return wide * 0.2 + (lid * 0.8) - (1.0 - pow(lid, 0.15) * squint) -func calc_eyelid_expanded_squeeze(bs: Dictionary, side: String, wide_key: String, lid_key: String, squint_key: String) -> void: - var value = calc_squeeze(bs, wide_key, lid_key, squint_key) +func _calc_eyelid_expanded_squeeze(bs: Dictionary, side: String, wide_key: String, lid_key: String, squint_key: String) -> void: + var value = _calc_squeeze(bs, wide_key, lid_key, squint_key) var target_key = side + "EyeLidExpandedSqueeze" if value > 0.8: bs[target_key] = bs[wide_key] @@ -344,8 +355,6 @@ func _map_blendshapes_to_unified() -> Dictionary: if not arkit_to_unified_mapping.has(blendshape): continue var unified_blendshape = arkit_to_unified_mapping[blendshape] - #if len(cached_valid_keys) > 0 and not cached_valid_keys.has(unified_blendshape): - # continue unified_blendshapes[unified_blendshape] = blendshapes[blendshape] return unified_blendshapes @@ -355,11 +364,6 @@ func _send_dirty_params(): var bundle : Array = [] for param in to_send_osc: - if param.binary_key == "JawOpen": - #print("JawOpen") - #print(param.value) - #print(param.key) - pass param.reset_dirty() # We send the message with the full path for the avatar parameter, and type. var type = param.type @@ -370,7 +374,6 @@ func _send_dirty_params(): else: type = "F" bundle.append(osc_client.prepare_osc_message(param.full_path, type, [param.value])) - #osc_client.send_osc_message(param.full_path, type, [param.value]) var send = osc_client.create_osc_bundle(3535, bundle) osc_client.send_osc_message_raw(send) @@ -508,7 +511,7 @@ func _avatar_params_request_complete(result : int, response_code : int, processing_request = false if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: - printerr("Request for VRC avatar params failed.") + printerr("[VRChat OSC] Request for VRC avatar params failed.") return print("[VRChat OSC] Avatar param request complete.") From 89c49d21bfef1dfac21a3555e47c8832d171517a Mon Sep 17 00:00:00 2001 From: Ellie Date: Fri, 3 Oct 2025 23:55:55 +0930 Subject: [PATCH 18/23] Rename mod, add start/stop support --- Mods/VRChatOSC/OSCQueryServer.gd | 32 ++++++++++----- .../{VRChatOSC.gd => VRChatFaceTracking.gd} | 40 +++++++++++++------ ...atOSC.gd.uid => VRChatFaceTracking.gd.uid} | 0 ...VRChatOSC.tscn => VRChatFaceTracking.tscn} | 22 +++++----- .../godot-multicast-dns/DNSRecord.gd | 4 +- .../godot-multicast-dns/MulticastDNS.gd | 10 ++++- 6 files changed, 71 insertions(+), 37 deletions(-) rename Mods/VRChatOSC/{VRChatOSC.gd => VRChatFaceTracking.gd} (94%) rename Mods/VRChatOSC/{VRChatOSC.gd.uid => VRChatFaceTracking.gd.uid} (100%) rename Mods/VRChatOSC/{VRChatOSC.tscn => VRChatFaceTracking.tscn} (63%) diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/OSCQueryServer.gd index a386d81..a1d5ed3 100644 --- a/Mods/VRChatOSC/OSCQueryServer.gd +++ b/Mods/VRChatOSC/OSCQueryServer.gd @@ -8,30 +8,42 @@ class_name OSCQueryServer @export var osc_server_ip : String = "127.0.0.1" @export var osc_server_port : int = 9001 +var running : bool = false signal on_host_info_requested signal on_root_requested signal on_osc_server_message_received(address : String, args) @export var http_server : HttpServer func _ready(): + start() + +func start() -> void: + running = true osc_server.change_port_and_ip(osc_server_port, osc_server_ip) osc_server.message_received.connect(_message_received) osc_server.start_server() - + var host_info_router = OSCQueryHostInfoRouter.new() host_info_router.query_server = self var address_router = OSCQueryAddressRouter.new() address_router.query_server = self - - http_server = HttpServer.new() - http_server.bind_address = "127.0.0.1" - http_server.port = 61613 # TODO: Make random port. This is advertised on mDNS to apps. - add_child(http_server) - - http_server.register_router("^/HOST_INFO", host_info_router) - http_server.register_router("^/", address_router) + + # Add if not already added. + if http_server == null: + http_server = HttpServer.new() + http_server.bind_address = "127.0.0.1" + http_server.port = 61613 # TODO: Make random port. This is advertised on mDNS to apps. + add_child(http_server) + http_server.register_router("^/HOST_INFO", host_info_router) + http_server.register_router("^/", address_router) + http_server.start() - + +func stop() -> void: + http_server.stop() + osc_server.stop_server() + running = false + func _message_received(address : String, args) -> void: on_osc_server_message_received.emit(address, args) diff --git a/Mods/VRChatOSC/VRChatOSC.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd similarity index 94% rename from Mods/VRChatOSC/VRChatOSC.gd rename to Mods/VRChatOSC/VRChatFaceTracking.gd index 6cb28ab..d6bf3ee 100644 --- a/Mods/VRChatOSC/VRChatOSC.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -232,15 +232,29 @@ func _apply_transform_rules(unified_blendshapes : Dictionary, base_dict : Dictio unified_blendshapes[param_name] = max_pos ParameterMappings.COMBINATION_TYPE.MIN: - var min : float = 1.1 # Very unlikely > 1.0 exists given they're constrainted to 1.0 + var min_num : float = 1.1 # Very unlikely > 1.0 exists given they're constrainted to 1.0 for shape_info : Dictionary in shapes: var shape : String = shape_info["shape"] var shape_type : ParameterMappings.SHAPE_KEY_TYPE = shape_info.get("shape_type", ParameterMappings.SHAPE_KEY_TYPE.UNIFIED) var value : float = _get_unified_value(shape, shape_type, unified_blendshapes) - if value < min: - min = value - unified_blendshapes[param_name] = min - + if value < min_num: + min_num = value + unified_blendshapes[param_name] = min_num + +func scene_init() -> void: + print("[VRChat Face Tracking] Starting services and requesting avatar parameters...") + if not osc_query_server.running: + osc_query_server.start() + if not dns_service.running: + dns_service.start() + previous_avatar_id = "" + _get_avatar_params() + +func scene_shutdown() -> void: + print("[VRChat Face Tracking] Stopping servers...") + osc_query_server.stop() + dns_service.stop() + func _ready() -> void: avatar_req = HTTPRequest.new() add_child(avatar_req) @@ -282,7 +296,7 @@ func _process(delta : float) -> void: return curr_client_send_time += delta - if curr_client_send_time > client_send_rate_limit_ms / 1000: + if curr_client_send_time > int(client_send_rate_limit_ms) / 1000: curr_client_send_time = 0 # Map the blendshapes we have from mediapipe to the unified versions. @@ -380,7 +394,7 @@ func _send_dirty_params(): func _osc_query_received(address : String, args) -> void: if address == "/avatar/change": - print("WAOH") + print("[VRChat Face Tracking] Avatar change detected via OSC Query Server.") _get_avatar_params() func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: @@ -494,7 +508,7 @@ func _vrc_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: # If it is the first time going through, we get the current avi params. _get_avatar_params() - print("[VRChat OSC] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) + print("[VRChat Face Tracking] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) func _get_avatar_params(): if vrchat_osc_query_endpoint == "": @@ -504,22 +518,22 @@ func _get_avatar_params(): var err = avatar_req.request(vrchat_osc_query_endpoint + "/avatar") processing_request = true if err != OK: - printerr("[VRChat OSC] Failed to request VRC avatar parameters with error code: %d" % err) + printerr("[VRChat Face Tracking] Failed to request VRC avatar parameters with error code: %d" % err) func _avatar_params_request_complete(result : int, response_code : int, headers: PackedStringArray, body: PackedByteArray) -> void: processing_request = false if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: - printerr("[VRChat OSC] Request for VRC avatar params failed.") + printerr("[VRChat Face Tracking] Request for VRC avatar params failed.") return - print("[VRChat OSC] Avatar param request complete.") + print("[VRChat Face Tracking] Avatar param request complete.") var json = JSON.parse_string(body.get_string_from_utf8()) var root_contents : Dictionary = json["CONTENTS"] if not root_contents.has("parameters") or not root_contents.has("change"): # Could be booting game/loading/logging in/not in game. - printerr("[VRChat OSC] No parameters, or avatar information exists.") + printerr("[VRChat Face Tracking] No parameters, or avatar information exists.") return # Uh oh... that's a lot of hardcoded values. @@ -528,7 +542,7 @@ func _avatar_params_request_complete(result : int, response_code : int, var has_changed_avi : bool = current_avatar_id != previous_avatar_id if has_changed_avi: # Update only if changed avi. - print("[VRChat OSC] Avatar has changed. Updating parameter keys, values and types.") + print("[VRChat Face Tracking] Avatar has changed. Updating parameter keys, values and types.") vrc_param_keys = [] cached_valid_keys = [] vrc_params.reset() diff --git a/Mods/VRChatOSC/VRChatOSC.gd.uid b/Mods/VRChatOSC/VRChatFaceTracking.gd.uid similarity index 100% rename from Mods/VRChatOSC/VRChatOSC.gd.uid rename to Mods/VRChatOSC/VRChatFaceTracking.gd.uid diff --git a/Mods/VRChatOSC/VRChatOSC.tscn b/Mods/VRChatOSC/VRChatFaceTracking.tscn similarity index 63% rename from Mods/VRChatOSC/VRChatOSC.tscn rename to Mods/VRChatOSC/VRChatFaceTracking.tscn index 6dd90e0..38707a9 100644 --- a/Mods/VRChatOSC/VRChatOSC.tscn +++ b/Mods/VRChatOSC/VRChatFaceTracking.tscn @@ -1,30 +1,30 @@ [gd_scene load_steps=6 format=3 uid="uid://cpe3ulnjnrapo"] -[ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatOSC.gd" id="1_x840n"] -[ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_b4fua"] -[ext_resource type="Script" uid="uid://goefhxca5k8g" path="res://Mods/VMCController/KiriOSC/KiriOSCClient.gd" id="3_faft7"] -[ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/OSCQueryServer.gd" id="4_ovfdg"] -[ext_resource type="Script" uid="uid://csrri2vhxv4w5" path="res://Mods/VMCController/KiriOSC/KiriOSCServer.gd" id="5_lpbnq"] +[ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatFaceTracking.gd" id="1_md8v4"] +[ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_xkeea"] +[ext_resource type="Script" uid="uid://goefhxca5k8g" path="res://Mods/VMCController/KiriOSC/KiriOSCClient.gd" id="3_435n5"] +[ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/OSCQueryServer.gd" id="4_shth5"] +[ext_resource type="Script" uid="uid://csrri2vhxv4w5" path="res://Mods/VMCController/KiriOSC/KiriOSCServer.gd" id="5_k104g"] -[node name="VrChatOsc" type="Node3D" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] -script = ExtResource("1_x840n") +[node name="VRChatFaceTracking" type="Node3D" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] +script = ExtResource("1_md8v4") dns_service = NodePath("MulticastDNS") osc_client = NodePath("KiriOSClient") osc_query_server = NodePath("OSCQueryServer") [node name="MulticastDNS" type="Node" parent="."] -script = ExtResource("2_b4fua") +script = ExtResource("2_xkeea") metadata/_custom_type_script = "uid://bfemeu7ysbxwc" [node name="KiriOSClient" type="Node" parent="."] -script = ExtResource("3_faft7") +script = ExtResource("3_435n5") metadata/_custom_type_script = "uid://goefhxca5k8g" [node name="OSCQueryServer" type="Node" parent="." node_paths=PackedStringArray("osc_server")] -script = ExtResource("4_ovfdg") +script = ExtResource("4_shth5") osc_server = NodePath("../KiriOSCServer") metadata/_custom_type_script = "uid://bg1sitssmsggs" [node name="KiriOSCServer" type="Node" parent="."] -script = ExtResource("5_lpbnq") +script = ExtResource("5_k104g") metadata/_custom_type_script = "uid://csrri2vhxv4w5" diff --git a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd index a9febaa..d481bf6 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/DNSRecord.gd @@ -35,8 +35,8 @@ static func from_packet(packet : StreamPeerBuffer, cache: Dictionary) -> DNSReco dns_record._ns_record(packet) elif dns_record.dns_type == RECORD_TYPE.TXT: dns_record._txt_record(packet) - else: - print("Unsupported DNS record type found: ", dns_record.dns_type) +# else: +# print("Unsupported DNS record type found: ", dns_record.dns_type) return dns_record diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index 63ffb2f..ef5d1d6 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -5,16 +5,24 @@ var server : UDPServer var clients : Array[PacketPeerUDP] = [] var multicast_address : String = "224.0.0.251" var local_addresses : Array[String] = [] - +var running : bool = false signal on_receive(packet : DNSPacket, raw_packet : StreamPeerBuffer) func _ready() -> void: + start() + +func start() -> void: + running = true server = UDPServer.new() # We only listen on ipv4, we're not using mDNS for IPv6. var err = server.listen(5353, "0.0.0.0") if err != OK: printerr("[Multicast DNS] Failed to start listening on port 5353 with error code %d" % err) +func stop() -> void: + server.stop() + running = false + func _process(delta : float) -> void: server.poll() # Important! if server.is_connection_available(): From 92ec2b04e829694115df0918a2c1abeaeda7951e Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 4 Oct 2025 20:35:32 +0930 Subject: [PATCH 19/23] Add server port settings --- Mods/VRChatOSC/OSCQueryServer.gd | 22 ++++++++++++++++++++-- Mods/VRChatOSC/VRChatFaceTracking.gd | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/OSCQueryServer.gd index a1d5ed3..52a6ea4 100644 --- a/Mods/VRChatOSC/OSCQueryServer.gd +++ b/Mods/VRChatOSC/OSCQueryServer.gd @@ -7,6 +7,7 @@ class_name OSCQueryServer @export var osc_paths : Dictionary = {} @export var osc_server_ip : String = "127.0.0.1" @export var osc_server_port : int = 9001 +@export var osc_query_server_port : int = 61613 var running : bool = false signal on_host_info_requested @@ -20,7 +21,8 @@ func _ready(): func start() -> void: running = true osc_server.change_port_and_ip(osc_server_port, osc_server_ip) - osc_server.message_received.connect(_message_received) + if len(osc_server.message_received.get_connections()) == 0: + osc_server.message_received.connect(_message_received) osc_server.start_server() var host_info_router = OSCQueryHostInfoRouter.new() @@ -32,7 +34,7 @@ func start() -> void: if http_server == null: http_server = HttpServer.new() http_server.bind_address = "127.0.0.1" - http_server.port = 61613 # TODO: Make random port. This is advertised on mDNS to apps. + http_server.port = osc_query_server_port add_child(http_server) http_server.register_router("^/HOST_INFO", host_info_router) http_server.register_router("^/", address_router) @@ -44,6 +46,22 @@ func stop() -> void: osc_server.stop_server() running = false +func set_osc_server_port(new_port : int) -> void: + if new_port != osc_server_port: + osc_server_port = new_port + stop() + start() + else: + osc_server_port = new_port + +func set_osc_query_server_port(new_port : int) -> void: + if new_port != osc_query_server_port: + osc_query_server_port = new_port + stop() + start() + else: + osc_query_server_port = new_port + func _message_received(address : String, args) -> void: on_osc_server_message_received.emit(address, args) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd index d6bf3ee..59187e3 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -7,6 +7,20 @@ class_name VRChatOSC @export var osc_query_server : OSCQueryServer # User Settings var send_eye_tracking : bool = true +var osc_server_listen_port : int = 9001: + get: + return osc_server_listen_port + set(value): + if osc_query_server != null: + osc_query_server.set_osc_server_port(osc_server_listen_port) + +var osc_query_server_listen_port : int = 61631: + get: + return osc_query_server_listen_port + set(value): + if osc_query_server != null: + osc_query_server.set_osc_query_server_port(osc_query_server_listen_port) + # Internal fields var osc_query_name : String = str(randi_range(500000, 5000000)) var osc_server_name : String = str(randi_range(500000, 5000000)) @@ -279,6 +293,8 @@ func _ready() -> void: arkit_to_unified_mapping[new_key] = new_value add_tracked_setting("send_eye_tracking", "Send eye tracking blendshapes") + add_tracked_setting("osc_query_server_listen_port", "OSC Query Server Listen Port") + add_tracked_setting("osc_server_listen_port", "OSC Server Listen Port") var force_avatar_detection_button : Button = Button.new() force_avatar_detection_button.text = "Force Avatar Refresh" From 0b948850152d7fccd1772dc563254c090e6a1b3e Mon Sep 17 00:00:00 2001 From: Ellie Date: Sat, 4 Oct 2025 20:47:58 +0930 Subject: [PATCH 20/23] Cleanup --- Mods/VRChatOSC/OSCQueryServer.gd | 17 +++++----- Mods/VRChatOSC/VRCParams.gd | 9 +++--- Mods/VRChatOSC/VRChatFaceTracking.gd | 48 ++++++++++++++-------------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/OSCQueryServer.gd index 52a6ea4..001acc0 100644 --- a/Mods/VRChatOSC/OSCQueryServer.gd +++ b/Mods/VRChatOSC/OSCQueryServer.gd @@ -1,20 +1,19 @@ extends Node class_name OSCQueryServer -# TODO: Export required? @export var osc_server : KiriOSCServer @export var app_name : String @export var osc_paths : Dictionary = {} @export var osc_server_ip : String = "127.0.0.1" @export var osc_server_port : int = 9001 @export var osc_query_server_port : int = 61613 +@export var http_server : HttpServer var running : bool = false signal on_host_info_requested signal on_root_requested signal on_osc_server_message_received(address : String, args) -@export var http_server : HttpServer func _ready(): start() @@ -53,7 +52,7 @@ func set_osc_server_port(new_port : int) -> void: start() else: osc_server_port = new_port - + func set_osc_query_server_port(new_port : int) -> void: if new_port != osc_query_server_port: osc_query_server_port = new_port @@ -61,15 +60,15 @@ func set_osc_query_server_port(new_port : int) -> void: start() else: osc_query_server_port = new_port - + func _message_received(address : String, args) -> void: on_osc_server_message_received.emit(address, args) - + class OSCQueryHostInfoRouter: extends HttpRouter var query_server : OSCQueryServer - - func handle_get(request: HttpRequest, response: HttpResponse): + + func handle_get(_request: HttpRequest, response: HttpResponse): query_server.on_host_info_requested.emit() var data = { "NAME": query_server.app_name, @@ -90,8 +89,8 @@ class OSCQueryHostInfoRouter: class OSCQueryAddressRouter: extends HttpRouter var query_server : OSCQueryServer - - func handle_get(request: HttpRequest, response: HttpResponse): + + func handle_get(_request: HttpRequest, response: HttpResponse): query_server.on_root_requested.emit() var data = { "DESCRIPTION": "Root", diff --git a/Mods/VRChatOSC/VRCParams.gd b/Mods/VRChatOSC/VRCParams.gd index 9830672..e1e7821 100644 --- a/Mods/VRChatOSC/VRCParams.gd +++ b/Mods/VRChatOSC/VRCParams.gd @@ -63,7 +63,7 @@ func initialize(raw_avatar_params : Dictionary, avatar_id : String, has_changed_ param ) pass - + func valid_params_from_dict(dict : Dictionary) -> Array[String]: var keys = dict.keys() var valid = _params.filter(func (p : VRCParam): return p.binary_key in keys) @@ -75,14 +75,13 @@ func valid_params_from_dict(dict : Dictionary) -> Array[String]: ## Updates a particular key to the supplied value. ## This func takes care of the exchange between binary/float parameters in VRC tracking. func update_value(key : String, value): - # TODO: Add cache for binary_key -> VRCParam. Make sure to reset in .reset method. var params : Array[VRCParam] = _params.filter(func (p : VRCParam): return p.binary_key == key) if len(params) == 0: return var param : VRCParam = params[0] - + if param.is_binary(): # This is actually an Array[VRCParam] but ... Godot... var param_group : Array = _binary_params[param.binary_key] @@ -97,13 +96,13 @@ func update_value(key : String, value): var neg_params : Array = param_group.filter(func (p : VRCParam): return p.binary_exponent == 0) if len(neg_params) == 1: neg_params[0].update_value(is_neg) - + param_group.sort_custom( func (a : VRCParam, b : VRCParam): return a.binary_exponent < b.binary_exponent ) - + # 1. Determine N (number of magnitude bits) var N : int = len(param_group.filter(func (p : VRCParam): return p.binary_exponent != 0)) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd index 59187e3..f430a1d 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -310,17 +310,17 @@ func _ready() -> void: func _process(delta : float) -> void: if vrchat_osc_query_endpoint == "": return - + curr_client_send_time += delta if curr_client_send_time > int(client_send_rate_limit_ms) / 1000: curr_client_send_time = 0 # Map the blendshapes we have from mediapipe to the unified versions. var unified_blendshapes : Dictionary = _map_blendshapes_to_unified() - + # Apply unified blendshape simplification mapping _apply_transform_rules(unified_blendshapes, ParameterMappings.simplified_parameter_mapping) - + if unified_blendshapes.has("MouthStretchRight") and unified_blendshapes.has("MouthStretchLeft") \ and unified_blendshapes.has("MouthSmileRight") and unified_blendshapes.has("MouthSmileLeft"): # Set MouthSadLeft/Right - complexish conversions @@ -334,7 +334,7 @@ func _process(delta : float) -> void: unified_blendshapes["MouthFrown"] > unified_blendshapes["MouthStretchLeft"] if unified_blendshapes["MouthFrown"] \ else unified_blendshapes["MouthStretchLeft"] - unified_blendshapes["MouthSmileLeft"]) - + if unified_blendshapes.has("EyeWideLeft") \ and unified_blendshapes.has("EyeWideRight") \ and unified_blendshapes.has("EyeLidLeft") \ @@ -392,7 +392,7 @@ func _map_blendshapes_to_unified() -> Dictionary: func _send_dirty_params(): var to_send_osc : Array[VRCParam] = vrc_params.get_dirty() var bundle : Array = [] - + for param in to_send_osc: param.reset_dirty() # We send the message with the full path for the avatar parameter, and type. @@ -407,16 +407,16 @@ func _send_dirty_params(): var send = osc_client.create_osc_bundle(3535, bundle) osc_client.send_osc_message_raw(send) - -func _osc_query_received(address : String, args) -> void: + +func _osc_query_received(address : String, _args) -> void: if address == "/avatar/change": print("[VRChat Face Tracking] Avatar change detected via OSC Query Server.") _get_avatar_params() -func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: +func _resolve_dns_packet(packet : DNSPacket, _raw_packet : StreamPeerBuffer) -> void: if vrchat_osc_query_endpoint == "" or packet.opcode != 0: return - + for question : DNSQuestion in packet.dns_questions: # We have two services to respond to: # 1. The OSC Query Server (http) (_oscjson._tcp.local) @@ -428,10 +428,10 @@ func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> v elif question.full_label.begins_with("_oscjson._tcp.local"): service_name = "SNEKS-" + osc_query_name is_osc_query = true - + if service_name == "": continue - + var full_name : Array[String] = [service_name, question.labels[0], question.labels[1], question.labels[2]] var full_service_name : Array[String] = [service_name, question.labels[0].replace("_", ""), question.labels[1].replace("_", "")] @@ -440,7 +440,7 @@ func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> v txt_record.labels = full_name txt_record.dns_type = DNSRecord.RECORD_TYPE.TXT txt_record.data = { "text": "txtvers=1" } - + var srv_record = DNSRecord.new() srv_record.labels = full_name srv_record.dns_type = DNSRecord.RECORD_TYPE.SRV @@ -449,7 +449,7 @@ func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> v else: srv_record.data = { "port": osc_query_server.osc_server_port } srv_record.data.set("target", full_service_name) - + var a_record = DNSRecord.new() a_record.dns_type = DNSRecord.RECORD_TYPE.A a_record.labels = full_service_name @@ -458,15 +458,15 @@ func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> v a_record.data = { "address": osc_query_server.http_server.bind_address } else: a_record.data = { "address": osc_query_server.osc_server_ip } - + var ptr_record = DNSRecord.new() ptr_record.dns_type = DNSRecord.RECORD_TYPE.PTR ptr_record.data = { "domain_labels": full_name } ptr_record.labels = question.labels - + var answers : Array[DNSRecord] = [ptr_record] var additional : Array[DNSRecord] = [txt_record, srv_record, a_record] - + var new_packet = DNSPacket.new() new_packet.dns_answers = answers new_packet.dns_additional = additional @@ -477,11 +477,11 @@ func _resolve_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> v new_packet.opcode = 0 new_packet.response_code = 0 new_packet.id = 0 - + # Send it off to our peers to alert them to the answer. dns_service.send_packet(new_packet) - -func _vrc_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: + +func _vrc_dns_packet(packet : DNSPacket, _raw_packet : StreamPeerBuffer) -> void: if not packet.query_response: return if len(packet.dns_answers) == 0 or len(packet.dns_additional) == 0: @@ -516,14 +516,14 @@ func _vrc_dns_packet(packet : DNSPacket, raw_packet : StreamPeerBuffer) -> void: ip_address, port ] - + if not osc_client.is_client_active(): # Init osc sender. Default to 9000 (default OSC port). osc_client.change_port_and_ip(9000, ip_address) osc_client.start_client() # If it is the first time going through, we get the current avi params. _get_avatar_params() - + print("[VRChat Face Tracking] Found VRChat OSC Query Endpoint: %s" % vrchat_osc_query_endpoint) func _get_avatar_params(): @@ -537,14 +537,14 @@ func _get_avatar_params(): printerr("[VRChat Face Tracking] Failed to request VRC avatar parameters with error code: %d" % err) func _avatar_params_request_complete(result : int, response_code : int, - headers: PackedStringArray, body: PackedByteArray) -> void: + _headers: PackedStringArray, body: PackedByteArray) -> void: processing_request = false - + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: printerr("[VRChat Face Tracking] Request for VRC avatar params failed.") return print("[VRChat Face Tracking] Avatar param request complete.") - + var json = JSON.parse_string(body.get_string_from_utf8()) var root_contents : Dictionary = json["CONTENTS"] if not root_contents.has("parameters") or not root_contents.has("change"): From e4e6253810baef7cd5130a5c20129910404b7f76 Mon Sep 17 00:00:00 2001 From: Ellie Date: Sun, 5 Oct 2025 22:03:39 +1030 Subject: [PATCH 21/23] Add basic head tracking --- Mods/VRChatOSC/VRChatFaceTracking.gd | 9 +++++- .../godot-multicast-dns/MulticastDNS.gd | 4 +-- Mods/VRChatOSC/godottpd/http_response.gd | 28 +++++++++---------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd index f430a1d..926a581 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -348,7 +348,14 @@ func _process(delta : float) -> void: # Apply legacy parameter mapping (this makes me sad) _apply_transform_rules(unified_blendshapes, ParameterMappings.legacy_parameter_mapping) - + + var tracker_dict : Dictionary = get_global_mod_data("trackers") + if tracker_dict.has("head"): + var head_basis : Basis = tracker_dict["head"].transform.basis + var basic_rot = head_basis.get_euler() + unified_blendshapes["HeadRotationX"] = -basic_rot.y + unified_blendshapes["HeadRotationY"] = -basic_rot.x + if len(cached_valid_keys) == 0: cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) diff --git a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd index ef5d1d6..7108e56 100644 --- a/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd +++ b/Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd @@ -23,7 +23,7 @@ func stop() -> void: server.stop() running = false -func _process(delta : float) -> void: +func _process(_delta : float) -> void: server.poll() # Important! if server.is_connection_available(): var receiver = server.take_connection() @@ -45,7 +45,7 @@ func _process(delta : float) -> void: # Make sure it is local, this may be disregarded in some situations in the future? # FIXME: If issues happen, remove this check. - var packet_ip = receiver.get_packet_ip() + var _packet_ip = receiver.get_packet_ip() if not local_addresses.has(receiver.get_packet_ip()): continue diff --git a/Mods/VRChatOSC/godottpd/http_response.gd b/Mods/VRChatOSC/godottpd/http_response.gd index 72a457c..2ec0c64 100644 --- a/Mods/VRChatOSC/godottpd/http_response.gd +++ b/Mods/VRChatOSC/godottpd/http_response.gd @@ -37,8 +37,8 @@ func send_raw(status_code: int, data: PackedByteArray = PackedByteArray([]), con client.put_data(("Server: %s\r\n" % server_identifier).to_ascii_buffer()) for header in headers.keys(): client.put_data(("%s: %s\r\n" % [header, headers[header]]).to_ascii_buffer()) - for cookie in cookies: - client.put_data(("Set-Cookie: %s\r\n" % cookie).to_ascii_buffer()) + for cookiez in cookies: + client.put_data(("Set-Cookie: %s\r\n" % cookiez).to_ascii_buffer()) client.put_data(("Content-Length: %d\r\n" % data.size()).to_ascii_buffer()) client.put_data("Connection: close\r\n".to_ascii_buffer()) client.put_data(("Access-Control-Allow-Origin: %s\r\n" % access_control_origin).to_ascii_buffer()) @@ -88,21 +88,21 @@ func set_header(field: StringName, value: Variant) -> void: ## [br][param options] - A Dictionary of [url=https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes]cookie attributes[/url] ## for this specific cokkie in the [code]{ "secure" : "true"}[/code] format. func cookie(name: String, value: String, options: Dictionary = {}) -> void: - var cookie: String = name+"="+value - if options.has("domain"): cookie+="; Domain="+options["domain"] - if options.has("max-age"): cookie+="; Max-Age="+options["max-age"] - if options.has("expires"): cookie+="; Expires="+options["expires"] - if options.has("path"): cookie+="; Path="+options["path"] - if options.has("secure"): cookie+="; Secure="+options["secure"] - if options.has("httpOnly"): cookie+="; HttpOnly="+options["httpOnly"] + var cookie_tmp: String = name+"="+value + if options.has("domain"): cookie_tmp+="; Domain="+options["domain"] + if options.has("max-age"): cookie_tmp+="; Max-Age="+options["max-age"] + if options.has("expires"): cookie_tmp+="; Expires="+options["expires"] + if options.has("path"): cookie_tmp+="; Path="+options["path"] + if options.has("secure"): cookie_tmp+="; Secure="+options["secure"] + if options.has("httpOnly"): cookie_tmp+="; HttpOnly="+options["httpOnly"] if options.has("sameSite"): match (options["sameSite"]): - true: cookie += "; SameSite=Strict" - "lax": cookie += "; SameSite=Lax" - "strict": cookie += "; SameSite=Strict" - "none": cookie += "; SameSite=None" + true: cookie_tmp += "; SameSite=Strict" + "lax": cookie_tmp += "; SameSite=Lax" + "strict": cookie_tmp += "; SameSite=Strict" + "none": cookie_tmp += "; SameSite=None" _: pass - cookies.append(cookie) + cookies.append(cookie_tmp) ## Automatically matches a "status_code" to an RFC 7231 compliant "status_text" From ea75cede1204153a8ee96ed52acb2e289d8a6eaf Mon Sep 17 00:00:00 2001 From: Ellie Date: Mon, 6 Oct 2025 12:16:56 +1030 Subject: [PATCH 22/23] Add head rotation Z axis --- Mods/VRChatOSC/VRChatFaceTracking.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd index 926a581..71e9b85 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -355,6 +355,7 @@ func _process(delta : float) -> void: var basic_rot = head_basis.get_euler() unified_blendshapes["HeadRotationX"] = -basic_rot.y unified_blendshapes["HeadRotationY"] = -basic_rot.x + unified_blendshapes["HeadRotationZ"] = basic_rot.z if len(cached_valid_keys) == 0: cached_valid_keys = vrc_params.valid_params_from_dict(unified_blendshapes) From 10186b08361126faabc5e0b95cc3693dd29c27f8 Mon Sep 17 00:00:00 2001 From: Ellie Date: Thu, 9 Oct 2025 07:10:04 +1030 Subject: [PATCH 23/23] Add mod description --- Mods/VRChatOSC/VRChatFaceTracking.gd | 1 - Mods/VRChatOSC/VRChatFaceTracking.tscn | 2 +- Mods/VRChatOSC/description.txt | 5 +++++ Mods/VRChatOSC/{ => osc-query}/OSCQueryServer.gd | 0 Mods/VRChatOSC/{ => osc-query}/OSCQueryServer.gd.uid | 0 5 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 Mods/VRChatOSC/description.txt rename Mods/VRChatOSC/{ => osc-query}/OSCQueryServer.gd (100%) rename Mods/VRChatOSC/{ => osc-query}/OSCQueryServer.gd.uid (100%) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.gd b/Mods/VRChatOSC/VRChatFaceTracking.gd index 71e9b85..a888e3a 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.gd +++ b/Mods/VRChatOSC/VRChatFaceTracking.gd @@ -105,7 +105,6 @@ func _get_unified_value(shape : String, shape_type : ParameterMappings.SHAPE_KEY func _get_unified_shape(shape: String, shape_type: ParameterMappings.SHAPE_KEY_TYPE) -> String: if shape_type == ParameterMappings.SHAPE_KEY_TYPE.UNIFIED: - return shape elif shape_type == ParameterMappings.SHAPE_KEY_TYPE.MEDIAPIPE: return arkit_to_unified_mapping.get(shape, shape) diff --git a/Mods/VRChatOSC/VRChatFaceTracking.tscn b/Mods/VRChatOSC/VRChatFaceTracking.tscn index 38707a9..d1b4138 100644 --- a/Mods/VRChatOSC/VRChatFaceTracking.tscn +++ b/Mods/VRChatOSC/VRChatFaceTracking.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://ysgfrvghy1n5" path="res://Mods/VRChatOSC/VRChatFaceTracking.gd" id="1_md8v4"] [ext_resource type="Script" uid="uid://bfemeu7ysbxwc" path="res://Mods/VRChatOSC/godot-multicast-dns/MulticastDNS.gd" id="2_xkeea"] [ext_resource type="Script" uid="uid://goefhxca5k8g" path="res://Mods/VMCController/KiriOSC/KiriOSCClient.gd" id="3_435n5"] -[ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/OSCQueryServer.gd" id="4_shth5"] +[ext_resource type="Script" uid="uid://bg1sitssmsggs" path="res://Mods/VRChatOSC/osc-query/OSCQueryServer.gd" id="4_shth5"] [ext_resource type="Script" uid="uid://csrri2vhxv4w5" path="res://Mods/VMCController/KiriOSC/KiriOSCServer.gd" id="5_k104g"] [node name="VRChatFaceTracking" type="Node3D" node_paths=PackedStringArray("dns_service", "osc_client", "osc_query_server")] diff --git a/Mods/VRChatOSC/description.txt b/Mods/VRChatOSC/description.txt new file mode 100644 index 0000000..133d82e --- /dev/null +++ b/Mods/VRChatOSC/description.txt @@ -0,0 +1,5 @@ +Implements sending of VRCFT-compatible parameters based on tracking to a locally running VRChat instance. + +Your VRChat avatar will need compatible parameters implemented prior to using this mod (i.e. /FT/v2/...). + +This mod also adds support for head tracking, specifically sending HeadRotationX, Y, Z as float parameters. diff --git a/Mods/VRChatOSC/OSCQueryServer.gd b/Mods/VRChatOSC/osc-query/OSCQueryServer.gd similarity index 100% rename from Mods/VRChatOSC/OSCQueryServer.gd rename to Mods/VRChatOSC/osc-query/OSCQueryServer.gd diff --git a/Mods/VRChatOSC/OSCQueryServer.gd.uid b/Mods/VRChatOSC/osc-query/OSCQueryServer.gd.uid similarity index 100% rename from Mods/VRChatOSC/OSCQueryServer.gd.uid rename to Mods/VRChatOSC/osc-query/OSCQueryServer.gd.uid