| … | … | @@ -17,10 +17,15 @@ A Godot plugin and example project for loading assets from the [hypercore-protoc |
| 17 | 17 | - [x] Example for saving data to hyper |
| 18 | 18 | - [x] Make FTP server that can be mounted as a network drive |
| 19 | 19 | - [x] Listen for changes (EventSource) |
| 20 | - [ ] Listen for download progress (EventSource) | |
| 21 | - [ ] Listen for / Publish extension messages (EventSource) | |
| 20 | - [x] Listen for download progress (EventSource) | |
| 21 | - [x] Listen for / Publish extension messages (EventSource) | |
| 22 | 22 | - [ ] Examples for all the methods exposed by the gateway. |
| 23 | - [x] GET/PUT text files | |
| 24 | - [x] Watch for changes | |
| 25 | - [ ] Basic chat app (WIP | |
| 23 | 26 | - [ ] Basic Multiplayer demo using extension messages |
| 27 | - [ ] Load gltf scene from hyper | |
| 28 | - [ ] Load VRM avatar from hyper (social multiplayer) [VRM stuff](https://github.com/V-Sekai/godot-vrm/blob/godot3/addons/vrm/import_vrm.gd) | |
| 24 | 29 | - [ ] Folder sync (From `hyper://` to fs, from fs to `hyper://`) |
| 25 | 30 | |
| 26 | 31 | ## Supported APIs: |
| … | … | @@ -0,0 +1,155 @@ |
| 1 | extends "res://src/ResizeRoot.gd" | |
| 2 | ||
| 3 | const LRU = preload('res://src/utils/LRU.gd') | |
| 4 | ||
| 5 | # Messages are JSON which contain {id, from, username, type, content} | |
| 6 | # All messages have a username and type | |
| 7 | # Some messages contain a `from` for peer that sent the message | |
| 8 | # If no `from` field is visible, it means it's from the sending peer | |
| 9 | # type: 'text` will have `content` of the text message that got sent | |
| 10 | # type: 'identity' will have empty `content`, and is there to notify peers of this users existence | |
| 11 | ||
| 12 | # GET peer list to start registering for extensions | |
| 13 | # Set up list of seen remotePublicKeys | |
| 14 | # Broadcast self after a second of waiting for peers | |
| 15 | # Broadcast self on name change | |
| 16 | # Broadcast self when seeing a new remotePublicKey | |
| 17 | ||
| 18 | export var archiveRoot = 'hyper://blog.mauve.moe/' | |
| 19 | export var extensionName = 'hyperchat-example-1' | |
| 20 | ||
| 21 | var extensionsFolder = archiveRoot + '$/extensions/' | |
| 22 | var extensionPath = extensionsFolder + extensionName | |
| 23 | ||
| 24 | var seen_messages = LRU.new() | |
| 25 | var seen_identities = LRU.new() | |
| 26 | ||
| 27 | onready var eventSource = $HyperGateway/HyperEventSource | |
| 28 | onready var gateway = $HyperGateway | |
| 29 | onready var chatbox = $ScrollContainer/CatBox | |
| 30 | onready var chatText = $HBoxContainer/ChatInput | |
| 31 | onready var username = $HBoxContainer/Username | |
| 32 | onready var broadcastRequest = $HyperGateway/BroadcastRequest | |
| 33 | onready var listPeersRequest = $HyperGateway/ListPeersRequest | |
| 34 | ||
| 35 | func _ready(): | |
| 36 | # Initialize random number generator | |
| 37 | randomize() | |
| 38 | gateway.ensureSetup() | |
| 39 | ||
| 40 | func display_item(from, content): | |
| 41 | # Add to item list | |
| 42 | chatText.text += from + ': ' + content + "\n" | |
| 43 | ||
| 44 | func send_current(): | |
| 45 | var text = chatText.text | |
| 46 | chatText.text = "" | |
| 47 | broadcast_text(text) | |
| 48 | ||
| 49 | func update_identity(from, username): | |
| 50 | # Have some sort of sidebar to display known users? | |
| 51 | display_item('System', 'Seen user ' + username) | |
| 52 | pass | |
| 53 | ||
| 54 | func list_peers(): | |
| 55 | listPeersRequest.request(extensionPath) | |
| 56 | ||
| 57 | func _on_text_message(data, from): | |
| 58 | # TODO: Something fancy with the `from` attribute? | |
| 59 | display_item(data.username, data.content) | |
| 60 | ||
| 61 | func _on_identity_message(data, from): | |
| 62 | var isNew = !has_seen_identity(from) | |
| 63 | ||
| 64 | # Track identity | |
| 65 | seen_identities.track(from) | |
| 66 | ||
| 67 | # If it's new, broadcast your own identity | |
| 68 | if isNew: | |
| 69 | broadcast_identity() | |
| 70 | ||
| 71 | func _on_unknown_message(data, from): | |
| 72 | # Emit some sort of error? | |
| 73 | printerr("Got unknown message", from, data) | |
| 74 | ||
| 75 | func get_username(): | |
| 76 | return username.text | |
| 77 | ||
| 78 | func broadcast_text(content): | |
| 79 | var username = get_username() | |
| 80 | broadcast_message({ | |
| 81 | 'type': 'text', | |
| 82 | 'content': content, | |
| 83 | 'username': username | |
| 84 | }) | |
| 85 | ||
| 86 | func broadcast_identity(): | |
| 87 | var username = get_username() | |
| 88 | broadcast_message({ | |
| 89 | 'type': 'identity', | |
| 90 | 'username': username | |
| 91 | }) | |
| 92 | ||
| 93 | func broadcast_message(message): | |
| 94 | # POST to extension URL | |
| 95 | if !message.has('id'): | |
| 96 | message.id = make_id() | |
| 97 | seen_messages.track(message.id) | |
| 98 | print('Broadcasting message', message) | |
| 99 | var body = JSON.print(message) | |
| 100 | broadcastRequest.request(extensionPath,[], false, HTTPClient.METHOD_GET, body) | |
| 101 | ||
| 102 | func has_seen_identity(from): | |
| 103 | return seen_identities.has(from) | |
| 104 | ||
| 105 | func has_seen_message(id): | |
| 106 | return seen_messages.has(id) | |
| 107 | ||
| 108 | func re_broadcast(message, from): | |
| 109 | if !message.has('from'): | |
| 110 | message.from = from | |
| 111 | broadcast_message(message) | |
| 112 | ||
| 113 | func handle_message(message, from): | |
| 114 | var finalFrom = message.from if message.has('from') else from | |
| 115 | if !message.has('id'): | |
| 116 | return _on_unknown_message(message, finalFrom) | |
| 117 | if !has_seen_message(message.id): | |
| 118 | re_broadcast(message, finalFrom) | |
| 119 | match message.type: | |
| 120 | 'text': | |
| 121 | _on_text_message(message, finalFrom) | |
| 122 | 'identity': | |
| 123 | _on_identity_message(message, finalFrom) | |
| 124 | _: | |
| 125 | _on_unknown_message(message, finalFrom) | |
| 126 | ||
| 127 | func _on_event(data, event, id): | |
| 128 | var parsed = JSON.parse(data) | |
| 129 | ||
| 130 | if parsed.error != OK: | |
| 131 | printerr("Unable to parse EventSource content " + parsed.error_line + "\n" + data) | |
| 132 | return | |
| 133 | ||
| 134 | handle_message(parsed.result, id) | |
| 135 | ||
| 136 | func make_id(): | |
| 137 | return str(randi()) + str(randi()) | |
| 138 | ||
| 139 | func start_listening(): | |
| 140 | eventSource.request(extensionsFolder) | |
| 141 | ||
| 142 | func _on_HyperEventSource_event(data, event, id): | |
| 143 | _on_event(data, event, id) | |
| 144 | ||
| 145 | func _on_HyperGateway_started_gateway(pid): | |
| 146 | start_listening() | |
| 147 | broadcast_identity() | |
| 148 | ||
| 149 | func _on_SendButton_pressed(): | |
| 150 | send_current() | |
| 151 | pass # Replace with function body. | |
| 152 | ||
| 153 | func _on_ChatInput_text_entered(_new_text): | |
| 154 | send_current() | |
| 155 | pass # Replace with function body. |
| … | … | @@ -0,0 +1,67 @@ |
| 1 | [gd_scene load_steps=5 format=2] | |
| 2 | ||
| 3 | [ext_resource path="res://src/HyperChat/HyperChat.gd" type="Script" id=1] | |
| 4 | [ext_resource path="res://src/HyperGateway/HyperGateway.tscn" type="PackedScene" id=2] | |
| 5 | [ext_resource path="res://src/HyperRequest/HyperEventSource.gd" type="Script" id=3] | |
| 6 | [ext_resource path="res://src/HyperRequest/HyperRequest.gd" type="Script" id=4] | |
| 7 | ||
| 8 | [node name="Control" type="VBoxContainer"] | |
| 9 | anchor_right = 1.0 | |
| 10 | anchor_bottom = 1.0 | |
| 11 | script = ExtResource( 1 ) | |
| 12 | __meta__ = { | |
| 13 | "_edit_use_anchors_": false | |
| 14 | } | |
| 15 | ||
| 16 | [node name="HBoxContainer" type="HBoxContainer" parent="."] | |
| 17 | margin_right = 666.0 | |
| 18 | margin_bottom = 24.0 | |
| 19 | ||
| 20 | [node name="ChatInput" type="LineEdit" parent="HBoxContainer"] | |
| 21 | margin_right = 487.0 | |
| 22 | margin_bottom = 24.0 | |
| 23 | size_flags_horizontal = 3 | |
| 24 | ||
| 25 | [node name="SendButton" type="Button" parent="HBoxContainer"] | |
| 26 | margin_left = 491.0 | |
| 27 | margin_right = 534.0 | |
| 28 | margin_bottom = 24.0 | |
| 29 | text = "Send" | |
| 30 | ||
| 31 | [node name="Username" type="LineEdit" parent="HBoxContainer"] | |
| 32 | margin_left = 538.0 | |
| 33 | margin_right = 666.0 | |
| 34 | margin_bottom = 24.0 | |
| 35 | rect_min_size = Vector2( 128, 0 ) | |
| 36 | text = "Anonymous" | |
| 37 | ||
| 38 | [node name="ScrollContainer" type="ScrollContainer" parent="."] | |
| 39 | margin_top = 28.0 | |
| 40 | margin_right = 666.0 | |
| 41 | margin_bottom = 666.0 | |
| 42 | size_flags_horizontal = 3 | |
| 43 | size_flags_vertical = 3 | |
| 44 | ||
| 45 | [node name="ChatBox" type="RichTextLabel" parent="ScrollContainer"] | |
| 46 | margin_right = 666.0 | |
| 47 | margin_bottom = 638.0 | |
| 48 | size_flags_horizontal = 3 | |
| 49 | size_flags_vertical = 3 | |
| 50 | text = "System: Enter some text and hit `send` to broadcast it to peers. | |
| 51 | " | |
| 52 | ||
| 53 | [node name="HyperGateway" parent="." instance=ExtResource( 2 )] | |
| 54 | ||
| 55 | [node name="HyperEventSource" type="Node" parent="HyperGateway"] | |
| 56 | script = ExtResource( 3 ) | |
| 57 | ||
| 58 | [node name="BroadcastRequest" type="HTTPRequest" parent="HyperGateway"] | |
| 59 | script = ExtResource( 4 ) | |
| 60 | ||
| 61 | [node name="ListPeersRequest" type="HTTPRequest" parent="HyperGateway"] | |
| 62 | script = ExtResource( 4 ) | |
| 63 | ||
| 64 | [connection signal="text_entered" from="HBoxContainer/ChatInput" to="." method="_on_ChatInput_text_entered"] | |
| 65 | [connection signal="pressed" from="HBoxContainer/SendButton" to="." method="_on_SendButton_pressed"] | |
| 66 | [connection signal="started_gateway" from="HyperGateway" to="." method="_on_HyperGateway_started_gateway"] | |
| 67 | [connection signal="event" from="HyperGateway/HyperEventSource" to="." method="_on_HyperEventSource_event"] |
| … | … | @@ -88,5 +88,5 @@ autoStart = true |
| 88 | 88 | [connection signal="started_gateway" from="HyperGateway" to="StatusLabel" method="setGatewayLoaded"] |
| 89 | 89 | [connection signal="starting_gateway" from="HyperGateway" to="StatusLabel" method="setGatewayLoading"] |
| 90 | 90 | [connection signal="loading_data" from="HyperGateway/HyperRequest" to="StatusLabel" method="setLoading"] |
| 91 | [connection signal="request_completed" from="HyperGateway/HyperRequest" to="StatusLabel" method="_on_HyperLoad_request_completed"] | |
| 92 | 91 | [connection signal="request_completed" from="HyperGateway/HyperRequest" to="ScrollContainer/Output" method="_on_HyperLoad_request_completed"] |
| 92 | [connection signal="request_completed" from="HyperGateway/HyperRequest" to="StatusLabel" method="_on_HyperLoad_request_completed"] |
| … | … | @@ -0,0 +1,42 @@ |
| 1 | # Least Recently Used Cache | |
| 2 | ## Use this if you want to keep track of a set, but with a limit | |
| 3 | ||
| 4 | # Map from value to time last used | |
| 5 | var values = {} | |
| 6 | const DEFAULT_SIZE = 512 | |
| 7 | ||
| 8 | var max_size = DEFAULT_SIZE | |
| 9 | ||
| 10 | func _init(new_max = DEFAULT_SIZE): | |
| 11 | max_size = new_max | |
| 12 | ||
| 13 | func has(value): | |
| 14 | return value in values | |
| 15 | pass | |
| 16 | ||
| 17 | func track(value): | |
| 18 | var time = OS.get_system_time_msecs() | |
| 19 | ||
| 20 | values[value] = time | |
| 21 | pass | |
| 22 | ||
| 23 | func is_full(): | |
| 24 | return size() > max_size | |
| 25 | ||
| 26 | func size(): | |
| 27 | return values.size() | |
| 28 | ||
| 29 | func clear_oldest(): | |
| 30 | var oldest = get_oldest() | |
| 31 | values.erase(oldest) | |
| 32 | pass | |
| 33 | ||
| 34 | func get_oldest(): | |
| 35 | var oldestTime = OS.get_system_time_msecs() | |
| 36 | var oldestValue = null | |
| 37 | for value in values.keys(): | |
| 38 | var valueTime = values[value] | |
| 39 | if valueTime < oldestTime: | |
| 40 | oldestTime = valueTime | |
| 41 | oldestValue = value | |
| 42 | return oldestValue |