Add WebGL and GOTM support

This commit is contained in:
Hamcha 2020-07-14 16:20:06 +02:00
parent d021b3ab3b
commit 5ca3c191cd
Signed by: hamcha
GPG Key ID: 41467804B19A3315
20 changed files with 1545 additions and 80 deletions

View File

@ -14,13 +14,13 @@ void fragment() {
if (col.r/col.g > 2.) {
if (length(cable_color) == 0.) {
if (UV.y > 0.6 && UV.y < 0.85) {
if (int(UV.x * 200.) % 12 > 4) {
if (float(int(UV.x * 200.) % 12) > 4.) {
col.rgb = vec3(0.94, 0.08, 0.08) * length(col.rgb);
} else {
col.rgb = vec3(0.04, 0.58, 0.98) * length(col.rgb);
}
} else {
if (int(UV.y * 200.) % 12 > 4) {
if (float(int(UV.y * 200.) % 12) > 4.) {
col.rgb = vec3(0.94, 0.08, 0.08) * length(col.rgb);
} else {
col.rgb = vec3(0.04, 0.58, 0.98) * length(col.rgb);
@ -32,7 +32,6 @@ void fragment() {
}
COLOR = col;
}"
custom_defines = ""
[sub_resource type="ShaderMaterial" id=2]
shader = SubResource( 1 )

View File

@ -4,6 +4,7 @@ var thread
var mutex
var sem
var gotm_mode = false
var time_max = 100 # Milliseconds.
var queue = []
@ -58,6 +59,8 @@ func cancel_resource(path):
func get_progress(path):
if gotm_mode:
return 1.0
_lock("get_progress")
var ret = -1
if path in pending:
@ -70,6 +73,8 @@ func get_progress(path):
func is_ready(path):
if gotm_mode:
return true
var ret
_lock("is_ready")
if path in pending:
@ -81,6 +86,8 @@ func is_ready(path):
func _wait_for_resource(res, path):
if gotm_mode:
return get_resource(path)
_unlock("wait_for_resource")
while true:
VisualServer.sync()
@ -92,6 +99,8 @@ func _wait_for_resource(res, path):
func get_resource(path):
if gotm_mode:
return load(path)
_lock("get_resource")
if path in pending:
if pending[path] is ResourceInteractiveLoader:
@ -140,8 +149,10 @@ func thread_func(_u):
thread_process()
func start():
mutex = Mutex.new()
sem = Semaphore.new()
thread = Thread.new()
thread.start(self, "thread_func", 0)
func start(gotm: bool):
gotm_mode = gotm
if not gotm:
mutex = Mutex.new()
sem = Semaphore.new()
thread = Thread.new()
thread.start(self, "thread_func", 0)

View File

@ -15,12 +15,14 @@ const SYSTEMS_UPDATE_INTERVAL = 10
const MASTER_SERVER_ADDR = "fgms.zyg.ovh"
const MASTER_SERVER_UDP_PORT = 9434
const MS_GAME_CODE = "odyssey-0-a1"
const GOTM_OVERRIDE = false
# Master server entry
var ms_active = false
var ms_key = ""
var server_name = ""
onready var gotm_mode = Gotm.is_live() or GOTM_OVERRIDE
var hosting = false
export var player_name = ""
@ -32,6 +34,8 @@ onready var scene_manager = $"/root/SceneManager"
func _ready():
player_name = "tider-" + str(randi() % 1000)
if gotm_mode:
Gotm.connect("lobby_changed", self, "_lobby_changed")
func bind_events():
get_tree().connect("network_peer_connected", self, "_player_connected")
@ -71,13 +75,23 @@ func host():
# Wait just a sec to draw
yield(get_tree().create_timer(0.3), "timeout")
print("Running UPNP magicks")
if discover_upnp() == UPNP.UPNP_RESULT_SUCCESS:
print("UPNP mapping added")
else:
push_warning("UPNP magicks fail, punching NAT in the face")
yield(punch_nat(), "completed")
# Run port forwarding/nat punchthrough if the platform doesn't do it already
if not gotm_mode:
print("Running UPNP magicks")
if discover_upnp() == UPNP.UPNP_RESULT_SUCCESS:
print("UPNP mapping added")
else:
push_warning("UPNP magicks fail, punching NAT in the face")
yield(punch_nat(), "completed")
server_name = player_name + "'s server"
player_info[1] = { "name": player_name }
round_info = { "map": "odyssey" }
if gotm_mode:
# Add to master server before hosting (in GOTM mode)
create_ms_entry()
bind_events()
var peer = NetworkedMultiplayerENet.new()
var server_res = peer.create_server(port, MAX_PLAYERS)
@ -91,9 +105,6 @@ func host():
get_tree().network_peer = peer
print("Hosting")
hosting = true
server_name = player_name + "'s server"
player_info[1] = { "name": player_name }
round_info = { "map": "odyssey" }
scene_manager.loading_text = null
@ -101,18 +112,27 @@ func host():
"res://Scenes/Maps/odyssey.tscn"
])
# Add to master server
create_ms_entry()
if not gotm_mode:
# Add to master server after hosting
create_ms_entry()
func join(addr: String):
func join(server):
scene_manager.enter_loader()
scene_manager.loading_text = "Joining server " + str(addr)
scene_manager.loading_text = "Joining server"
# Wait just a sec to draw
yield(get_tree().create_timer(0.3), "timeout")
bind_events()
var peer = NetworkedMultiplayerENet.new()
var addr = null
if gotm_mode:
var success = yield(server.join(), "completed")
addr = Gotm.lobby.host.address
else:
addr = server.addr
peer.create_client(addr, SERVER_PORT)
get_tree().network_peer = peer
print("Connecting to ", addr)
@ -172,15 +192,20 @@ func _ms_request(endpoint: String, data):
push_error("An error occurred in the HTTP request.")
func ms_get_entries():
var http_request = HTTPRequest.new()
add_child(http_request)
http_request.connect("request_completed", self, "_ms_response", ["list_games"])
var error = http_request.request(
"https://" + MASTER_SERVER_ADDR + "/" + MS_GAME_CODE,
["Content-Type: application/json"],
true, HTTPClient.METHOD_GET)
if error != OK:
push_error("An error occurred in the HTTP request.")
if gotm_mode:
var fetch = GotmLobbyFetch.new()
var lobbies = yield(fetch.first(), "completed")
emit_signal("ms_updated", "list_games", lobbies)
else:
var http_request = HTTPRequest.new()
add_child(http_request)
http_request.connect("request_completed", self, "_ms_response", ["list_games"])
var error = http_request.request(
"https://" + MASTER_SERVER_ADDR + "/" + MS_GAME_CODE,
["Content-Type: application/json"],
true, HTTPClient.METHOD_GET)
if error != OK:
push_error("An error occurred in the HTTP request.")
func get_game_data():
return {
@ -191,18 +216,28 @@ func get_game_data():
}
func create_ms_entry():
_ms_request("new", {
"game_id": MS_GAME_CODE,
"data": get_game_data()
})
if gotm_mode:
Gotm.host_lobby(true)
Gotm.lobby.hidden = false
else:
_ms_request("new", {
"game_id": MS_GAME_CODE,
"data": get_game_data()
})
update_ms_entry()
func update_ms_entry():
if ms_active:
_ms_request("update", {
"key": ms_key,
"data": get_game_data()
})
if gotm_mode:
var data = get_game_data()
Gotm.lobby.name = data.name
for key in data:
Gotm.lobby.set_property(key, data[key])
else:
if ms_active:
_ms_request("update", {
"key": ms_key,
"data": get_game_data()
})
var time_left = 30
func _process(delta):
@ -240,3 +275,6 @@ func get_current_map():
return GameWorld.Map.RUNTIME
_:
return GameWorld.Map.EMPTY
func _lobby_changed():
print("Lobby changed ", Gotm.lobby)

View File

@ -6,27 +6,33 @@ var loading_text = null
var loader = preload("res://Scenes/Loader.tscn")
onready var netgame = $"/root/Multiplayer"
func _ready() -> void:
queue.start()
queue.start(netgame.gotm_mode)
func enter_loader() -> void:
get_tree().change_scene_to(loader)
func load_scene(scene_path: String, dependencies: Array) -> void:
target_scene = scene_path
queue.queue_resource(scene_path)
for dep in dependencies:
queue.queue_resource(dep)
if not netgame.gotm_mode:
target_scene = scene_path
queue.queue_resource(scene_path)
for dep in dependencies:
queue.queue_resource(dep)
else:
get_tree().change_scene(scene_path)
func _physics_process(_delta: float) -> void:
if target_scene != null:
var remaining = queue.pending.size()
for path in queue.pending:
if queue.is_ready(path):
remaining -= 1
if remaining == 0:
get_tree().change_scene_to(queue.get_resource(target_scene))
target_scene = null
if not netgame.gotm_mode:
if target_scene != null:
var remaining = queue.pending.size()
for path in queue.pending:
if queue.is_ready(path):
remaining -= 1
if remaining == 0:
get_tree().change_scene_to(queue.get_resource(target_scene))
target_scene = null
func get_progress() -> String:
if loading_text != null:

View File

@ -4,7 +4,7 @@
[ext_resource path="res://Graphics/UI/iosevka-aile-regular.ttf" type="DynamicFontData" id=2]
[ext_resource path="res://Scenes/Loader.gd" type="Script" id=3]
[sub_resource type="Shader" id=2]
[sub_resource type="Shader" id=1]
code = "shader_type canvas_item;
uniform vec2 tex_size;
@ -14,23 +14,22 @@ void fragment() {
vec2 uv_adjusted = (UV - (uv_rect.xy / tex_size)) * (tex_size / uv_rect.zw);
float dist = distance(uv_adjusted, vec2(0.5));
if (dist < 0.26) {
COLOR = vec4(1);
COLOR = vec4(1.);
} else {
COLOR = texture(TEXTURE, UV);
}
}"
custom_defines = ""
[sub_resource type="ShaderMaterial" id=3]
shader = SubResource( 2 )
[sub_resource type="ShaderMaterial" id=2]
shader = SubResource( 1 )
shader_param/tex_size = Vector2( 240, 180 )
shader_param/uv_rect = Plane( 126, 16, 82, 84 )
[sub_resource type="AtlasTexture" id=1]
[sub_resource type="AtlasTexture" id=3]
atlas = ExtResource( 1 )
region = Rect2( 126, 16, 82, 84 )
[sub_resource type="Shader" id=5]
[sub_resource type="Shader" id=4]
code = "shader_type canvas_item;
uniform vec2 tex_size;
@ -47,23 +46,21 @@ void fragment() {
COLOR = vec4(tex.aaa, 1.-tex.a);
}
}"
custom_defines = ""
[sub_resource type="ShaderMaterial" id=6]
shader = SubResource( 5 )
[sub_resource type="ShaderMaterial" id=5]
shader = SubResource( 4 )
shader_param/tex_size = Vector2( 240, 180 )
shader_param/uv_rect = Plane( 146, 39, 39, 38 )
[sub_resource type="AtlasTexture" id=4]
[sub_resource type="AtlasTexture" id=6]
atlas = ExtResource( 1 )
region = Rect2( 146, 39, 39, 38 )
[sub_resource type="DynamicFont" id=8]
[sub_resource type="DynamicFont" id=7]
size = 32
font_data = ExtResource( 2 )
[sub_resource type="Animation" id=7]
resource_name = "spinner"
[sub_resource type="Animation" id=8]
length = 7.0
loop = true
tracks/0/type = "value"
@ -102,26 +99,26 @@ __meta__ = {
}
[node name="logo-temp-pixel2" type="TextureRect" parent="BottomRight"]
material = SubResource( 3 )
material = SubResource( 2 )
margin_left = -7.11853
margin_top = -6.38116
margin_right = 92.8815
margin_bottom = 93.6188
rect_pivot_offset = Vector2( 50, 50 )
texture = SubResource( 1 )
texture = SubResource( 3 )
stretch_mode = 4
__meta__ = {
"_edit_use_anchors_": false
}
[node name="logo-temp-pixel" type="TextureRect" parent="BottomRight"]
material = SubResource( 6 )
material = SubResource( 5 )
margin_left = -1.5
margin_top = -1.0
margin_right = 88.5
margin_bottom = 89.0
rect_pivot_offset = Vector2( 79, 59 )
texture = SubResource( 4 )
texture = SubResource( 6 )
stretch_mode = 4
__meta__ = {
"_edit_use_anchors_": false
@ -133,10 +130,10 @@ margin_top = 63.0
margin_right = 174.0
margin_bottom = 104.0
rect_scale = Vector2( 0.5, 0.5 )
custom_fonts/font = SubResource( 8 )
custom_fonts/font = SubResource( 7 )
custom_colors/font_color = Color( 1, 1, 1, 1 )
align = 2
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
autoplay = "spinner"
anims/spinner = SubResource( 7 )
anims/spinner = SubResource( 8 )

View File

@ -21,6 +21,10 @@ func _ready() -> void:
$"/root/Music/BGM".play()
netgame.connect("ms_updated", self, "_ms_update")
request_servers()
if netgame.gotm_mode:
# Hide manual connect
$Popup/MarginContainer/VBoxContainer/Label2.visible = false
$Popup/MarginContainer/VBoxContainer/HBoxContainer.visible = false
func _process(delta: float) -> void:
refresh_server_remaining -= delta
@ -45,9 +49,14 @@ func _ms_update(action, result):
if action == "list_games":
# Reset server list
server_list.clear()
servers = result
for server in servers:
server_list.add_item(server.data.name + " (" + server.address + ") - " + str(server.data.players) + "/" + str(server.data.max_players) + " players")
if netgame.gotm_mode:
servers = result
for server in servers:
server_list.add_item(server.name + " (" + server.id + ") - " + str(server.peers.size()) + "/" + str(server.get_property("max_players")) + " players")
else:
servers = result
for server in servers:
server_list.add_item(server.data.name + " (" + server.address + ") - " + str(server.data.players) + "/" + str(server.data.max_players) + " players")
func set_scale(val) -> void:
scale = val
@ -67,9 +76,9 @@ func _host_pressed() -> void:
func _join_pressed() -> void:
$Popup.popup_centered_ratio()
func join_server(addr: String) -> void:
func join_server(server) -> void:
$"/root/Music/BGM".stop()
$"/root/Multiplayer".join(addr)
netgame.join(server)
func _server_addr_changed(new_text: String) -> void:
$Popup/MarginContainer/VBoxContainer/HBoxContainer/Button.disabled = new_text.length() < 1
@ -78,4 +87,5 @@ func _manual_join_pressed():
join_server($Popup/MarginContainer/VBoxContainer/HBoxContainer/LineEdit.text)
func _server_item_clicked(index):
join_server(servers[index].address)
$"/root/Music/BGM".stop()
join_server(servers[index])

View File

@ -168,6 +168,7 @@ custom_fonts/font = SubResource( 6 )
text = "Join an existing game"
[node name="Popup" type="PopupDialog" parent="."]
visible = true
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 100.0

View File

@ -43,7 +43,7 @@ application/trademarks=""
[preset.1]
name="HTML5"
name="GOTM"
platform="HTML5"
runnable=true
custom_features=""

117
gotm/Gotm.gd Normal file
View File

@ -0,0 +1,117 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
extends Node
#warnings-disable
# Official GDScript API for games on gotm.io
# This plugin serves as a polyfill while developing against the API locally.
# The 'real' API calls are only available when running the game live on gotm.io.
# Running the game in the web player (gotm.io/web-player) also counts as live.
# Add this script as a global autoload. Make sure the global autoload is named
# "Gotm". It must be named "Gotm" for it to work.
##############################################################
# SIGNALS
##############################################################
# You connected or disconnected from a lobby. Access it at 'Gotm.lobby'
signal lobby_changed()
# Files were drag'n'dropped into the screen.
# The 'files' argument is an array of 'GotmFile'.
signal files_dropped(files, screen)
##############################################################
# PROPERTIES
##############################################################
# These are all read only.
# Player information.
var user: GotmUser = GotmUser.new()
# Current lobby you are in.
# Is null when not in a lobby.
var lobby: GotmLobby = null
##############################################################
# METHODS
##############################################################
# The API is live when the game runs on gotm.io.
# Running the game in the web player (gotm.io/web-player) also counts as live.
func is_live() -> bool:
return false
# Create a new lobby and join it.
#
# If 'show_invitation' is true, show an invitation link in a popup.
#
# By default, the lobby is hidden and is only accessible directly through
# its 'invite_link'.
# Set 'lobby.hidden' to false to make it fetchable with 'GotmLobbyFetch'.
#
# Returns the hosted lobby (also accessible at 'Gotm.lobby').
static func host_lobby(show_invitation: bool = true) -> GotmLobby:
return _GotmImpl._host_lobby(GotmLobby.new())
# Play an audio snippet with 'message' as a synthesized voice.
# 'language' is in BCP 47 format (e.g. "en-US" for american english).
# If specified language is not available "en-US" is used.
# Return true if playback succeeded.
func text_to_speech(message: String, language: String = "en-US") -> bool:
return true # pretend it worked
# Asynchronously open up the browser's file picker.
#
# If 'types' is specified, limit the file picker to files with matching file
# types (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers).
# If 'only_one' is true, only allow the user to pick one file.
#
# If a picking-session is already in progress, an empty
# array is asynchronously returned.
#
# Asynchronously return an array of 'GotmFile'.
# Use 'yield(pick_files(), "completed")' to retrieve the return value.
func pick_files(types: Array = Array(), only_one: bool = false) -> Array:
yield(get_tree().create_timer(0.25), "timeout")
return []
##############################################################
# PRIVATE
##############################################################
func _ready() -> void:
_GotmImpl._initialize(GotmLobby, GotmUser)
func _process(delta) -> void:
_GotmImpl._process()
var _impl: Dictionary = {}

65
gotm/GotmDebug.gd Normal file
View File

@ -0,0 +1,65 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name GotmDebug
#warnings-disable
# Helper library for testing against the API locally, as if it would be live.
#
# These functions do not make real API calls. They fake operations and
# trigger relevant signals as if they happened live.
#
# These functions do nothing when the game is running live on gotm.io.
# Running the game in the web player (gotm.io/web-player) also counts as live.
# Host a lobby without joining it.
# Note that the lobby is hidden by default and not fetchable with
# 'GotmLobbyFetch'. To make it fetchable, set 'hidden' to false.
# The lobby is only fetchable and joinable in this game process.
# Returns added lobby.
static func add_lobby() -> GotmLobby:
return _GotmDebugImpl._add_lobby(GotmLobby.new())
# Remove a lobby created with 'add_lobby', as if its host (you) disconnected from it.
# Triggers 'lobby_changed' if you are in that lobby.
static func remove_lobby(lobby: GotmLobby) -> void:
_GotmDebugImpl._remove_lobby(lobby)
# Remove all lobbies.
static func clear_lobbies() -> void:
_GotmDebugImpl._clear_lobbies()
# Add yourself to the lobby, without joining it.
# Triggers 'peer_joined' if you are in that lobby.
# Returns joined peer.
static func add_lobby_peer(lobby: GotmLobby) -> GotmUser:
return _GotmDebugImpl._add_lobby_player(lobby, GotmUser.new())
# Remove a peer created with 'add_lobby_peer' from the lobby, as if the peer (you) disconnected
# from the lobby.
# Triggers 'peer_left' if you are in that lobby.
static func remove_lobby_peer(lobby: GotmLobby, peer: GotmUser) -> void:
_GotmDebugImpl._remove_lobby_player(lobby, peer)

50
gotm/GotmFile.gd Normal file
View File

@ -0,0 +1,50 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name GotmFile
#warnings-disable
# A simple in-memory file descriptor used by 'Gotm.pick_files' and
# 'Gotm.files_dropped'.
##############################################################
# PROPERTIES
##############################################################
# File name.
var name: String
# File data.
var data: PoolByteArray
# Last time the file was modified in unix time (seconds since epoch).
var modified_time: int
##############################################################
# METHODS
##############################################################
# Save the file to the browser's download folder.
func download() -> void:
pass

155
gotm/GotmLobby.gd Normal file
View File

@ -0,0 +1,155 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name GotmLobby
#warnings-disable
# A lobby is a way of connecting players with eachother as if they
# were on the same local network.
#
# Lobbies can be joined either directly through an 'invite_link', or by
# joining lobbies fetched with the 'GotmLobbyFetch' class.
##############################################################
# SIGNALS
##############################################################
# Peer joined the lobby.
# 'peer_user' is a 'GotmUser' instance.
# This is only emitted if you are in this lobby.
signal peer_joined(peer_user)
# Peer left the lobby.
# 'peer_user' is a 'GotmUser' instance.
# This is only emitted if you are in this lobby.
signal peer_left(peer_user)
##############################################################
# READ-ONLY PROPERTIES
##############################################################
# Globally unique identifier.
var id: String
# Other peers in the lobby with addresses.
# Is an array of 'GotmUser'.
var peers: Array = []
# You with address.
var me: GotmUser = GotmUser.new()
# Host user with address.
var host: GotmUser = GotmUser.new()
# Peers can join the lobby directly through this link.
var invite_link: String
##############################################################
# WRITABLE PROPERTIES
##############################################################
# Note that only the host can write to these properties.
# Name that is searchable using 'GotmLobbyFetch'
# Names longer than 64 characters are truncated.
var name: String = ""
# Prevent the lobby from showing up in fetches?
# Peers may still join directly through 'invite_link'
var hidden: bool = true
# Prevent new peers from joining?
# Also prevents the lobby from showing up in fetches.
var locked: bool = false
##############################################################
# METHODS
##############################################################
# Asynchronously join this lobby after leaving current lobby.
#
# Use 'var success = yield(lobby.join(), "completed")' to wait for the call to complete
# and retrieve the return value.
#
# Sets 'Gotm.lobby' to the joined lobby if successful.
#
# Asyncronously returns true if successful, else false.
func join() -> bool:
return yield(_GotmImpl._join_lobby(self), "completed")
# Leave this lobby.
func leave() -> void:
_GotmImpl._leave_lobby(self)
# Am I the host of this lobby?
func is_host() -> bool:
return _GotmImpl._is_lobby_host(self)
# Get a custom property.
func get_property(name: String):
return _GotmImpl._get_lobby_property(self, name)
################################
# Host-only methods
################################
# Kick peer from this lobby.
# Returns true if successful, else false.
func kick(peer: GotmUser) -> bool:
return _GotmImpl._kick_lobby_peer(self, peer)
# Store up to 10 of your own custom properties in the lobby.
# These are visible to other peers when fetching lobbies.
# Only properties of types String, int, float or bool are allowed.
# Integers are converted to floats.
# Strings longer than 64 characters are truncated.
# Setting 'value' to null removes the property.
func set_property(name: String, value) -> void:
_GotmImpl._set_lobby_property(self, name, value)
# Make this lobby filterable by a custom property.
# Filtering is done when fetching lobbies with 'GotmLobbyFetch'.
# Up to 3 properties can be set as filterable at once.
func set_filterable(property_name: String, filterable: bool = true) -> void:
_GotmImpl._set_lobby_filterable(self, property_name, filterable)
# Make this lobby sortable by a custom property.
# Sorting is done when fetching lobbies with 'GotmLobbyFetch'.
# Up to 3 properties can be set as sortable at once.
func set_sortable(property_name: String, sortable: bool = true) -> void:
_GotmImpl._set_lobby_sortable(self, property_name, sortable)
################################
# PRIVATE
################################
var _impl: Dictionary = {}

122
gotm/GotmLobbyFetch.gd Normal file
View File

@ -0,0 +1,122 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name GotmLobbyFetch
#warnings-disable
# Used for fetching non-hidden and non-locked lobbies.
##############################################################
# PROPERTIES
##############################################################
################
# Filter options
################
# If not empty, fetch lobbies whose 'Lobby.name' contains 'name'.
var filter_name: String = ""
# If not empty, fetch lobbies whose filterable custom properties
# matches those in 'filter_properties'.
#
# For example, setting 'filter_properties.difficulty = 2' will
# only fetch lobbies that have been set up with both 'lobby.set_property("difficulty", 2)'
# and 'lobby.set_filterable("difficulty", true)'.
#
# If your lobby has multiple filterable props, you must provide every filterable
# prop in 'filter_properties'. Setting a prop's value to 'null' will match any
# value of that prop.
var filter_properties: Dictionary = {}
################
# Sort options
################
# If not empty, sort by a sortable custom property.
#
# For example, setting 'sort_property = "difficulty"' will
# only fetch lobbies that have been set up with both 'lobby.set_property("difficulty", some_value)'
# and 'lobby.set_sortable("difficulty", true)'.
#
# If your lobby has a sortable prop, you must always provide a 'sort_property'.
var sort_property: String = ""
# Sort results in ascending order?
var sort_ascending: bool = false
# If not null, fetch lobbies whose sort property's value is equal to or greater than 'sort_min'.
var sort_min = null
# If not null, fetch lobbies whose sort property's value is equal to or lesser than 'sort_max'.
var sort_max = null
# If true, and 'sort_min' is provided, exclude lobbies whose sort property's value is equal to 'sort_min'.
var sort_min_exclusive = false
# If true, and 'sort_max' is provided, exclude lobbies whose sort property's value is equal to 'sort_max'.
var sort_max_exclusive = false
##############################################################
# METHODS
##############################################################
# All these methods asynchronously fetch up to 8 non-hidden
# and non-locked lobbies.
#
# Modifying any filtering or sorting option resets the state of this
# 'GotmLobbyFetch' instance and causes the next fetch call to
# fetch the first lobbies.
#
# All calls asynchronously return an array of fetched lobbies.
# Use 'yield(fetch.next(), "completed")' to retrieve it.
# Fetch the next lobbies, starting after the last lobby fetched
# in the previous call.
func next(count: int = 8) -> Array:
return yield(_GotmImpl._fetch_lobbies(self, count, "next"), "completed")
# Fetch the previous lobbies, ending before the first lobby
# that was fetched in the previous call.
func previous(count: int = 8) -> Array:
return yield(_GotmImpl._fetch_lobbies(self, count, "previous"), "completed")
# Fetch the first lobbies.
func first(count: int = 8) -> Array:
return yield(_GotmImpl._fetch_lobbies(self, count, "first"), "completed")
# Fetch lobbies at the current position.
# Useful for refreshing lobbies without changing the page.
func current(count: int = 8) -> Array:
return yield(_GotmImpl._fetch_lobbies(self, count, "current"), "completed")
##############################################################
# PRIVATE
##############################################################
var _impl: Dictionary = {}

52
gotm/GotmUser.gd Normal file
View File

@ -0,0 +1,52 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name GotmUser
#warnings-disable
# Holds information about a Gotm user.
##############################################################
# PROPERTIES
##############################################################
# These are all read-only.
# Globally unique ID.
# Is empty if user is not logged in.
var id: String = ""
# Current nickname.
var display_name: String = ""
# The IP address of the user.
# Is empty if you are not in the same lobby.
var address: String = ""
# Is user logged in?
var is_logged_in: bool = false
##############################################################
# PRIVATE
##############################################################
var _impl: Dictionary = {}

21
gotm/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2020 Macaroni Studios AB
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.

30
gotm/README.md Normal file
View File

@ -0,0 +1,30 @@
<p align="center">
<a href="https://gotm.io"><img src="https://i.imgur.com/mc8HAgS.png" alt="gotm.io"></a>
<br/>
Access Gotm's API with GDScript
<br />
<a href="https://gotm.io/about/plugin">Learn more</a>
</p>
# Install
_Supports versions 3.1.0 and newer._
## 1. Download
Download the plugin from the [AssetLib](https://docs.godotengine.org/en/stable/tutorials/assetlib/using_assetlib.html#in-the-editor) in the Godot Editor and follow its instructions.
You can also download it directly [here](https://github.com/PlayGotm/GDGotm/archive/master.zip). Extract the contents into your project's directory.
## 2. Setup
Add Gotm.gd to your autoloads at "Project Settings -> AutoLoad". Make sure the global autoload is named "Gotm". It must be named "Gotm" for it to work.
# Examples
[Examples](https://github.com/PlayGotM/game-examples)
# Notes
This plugin serves as a polyfill when developing against the API locally.
The "real" API calls are only available when running the game live on gotm.io.

View File

@ -0,0 +1,108 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name _GotmDebugImpl
#warnings-disable
static func _login() -> void:
var g = _GotmImpl._get_gotm()
if g.is_live():
return
_logout()
g.user_id = _GotmImpl._generate_id()
g.emit_signal("user_changed")
static func _logout() -> void:
var g = _GotmImpl._get_gotm()
if g.is_live():
return
if !g.has_user():
return
g.user_id = ""
g.emit_signal("user_changed")
static func _add_lobby(lobby):
var g = _GotmImpl._get_gotm()
if g.is_live():
return lobby
lobby = _GotmImpl._add_lobby(lobby)
lobby._impl.address = "127.0.0.1"
lobby._impl.host_id = g.user._impl.id
lobby.host._impl.id = g.user._impl.id
lobby.peers = []
return lobby
static func _remove_lobby(lobby) -> void:
var g = _GotmImpl._get_gotm()
if g.is_live():
return
_GotmImpl._leave_lobby(lobby)
g._impl.lobbies.erase(lobby)
static func _clear_lobbies() -> void:
var g = _GotmImpl._get_gotm()
if g.is_live():
return
for lobby in g._impl.lobbies.duplicate():
_remove_lobby(lobby)
static func _add_lobby_player(lobby, peer):
var g = _GotmImpl._get_gotm()
if g.is_live():
return null
peer.address = "127.0.0.1"
peer._impl.id = peer._impl.id
lobby.peers.push_back(peer)
if lobby == g.lobby:
lobby.emit_signal("peer_joined", peer)
return peer
static func _remove_lobby_player(lobby, peer) -> void:
var g = _GotmImpl._get_gotm()
if g.is_live():
return
for p in lobby.peers.duplicate():
if peer._impl.id != p._impl.id:
continue
if peer._impl.id == lobby.host._impl.id:
_remove_lobby(lobby)
else:
lobby.peers.erase(p)
if lobby == g.lobby:
lobby.emit_signal("peer_left", p)

586
gotm/_impl/_GotmImpl.gd Normal file
View File

@ -0,0 +1,586 @@
# MIT License
#
# Copyright (c) 2020-2020 Macaroni Studios AB
#
# 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.
class_name _GotmImpl
#warnings-disable
# Utility sorter for 'sort_custom'.
class LobbySorter:
var fetch
var g
func sort(lhs, rhs) -> bool:
var a
var b
if fetch.sort_property.empty():
a = lhs._impl.created
b = rhs._impl.created
else:
a = lhs._impl.props[fetch.sort_property]
b = rhs._impl.props[fetch.sort_property]
if fetch.sort_ascending:
return _GotmImplUtility.is_less(a, b)
else:
return _GotmImplUtility.is_greater(a, b)
# Generate 20 characters long random string.
static func _generate_id() -> String:
var g = _get_gotm()
var id: String = ""
for i in range(20):
id += g._impl.chars[g._impl.rng.randi() % g._impl.chars.length()]
return id
# Retrieve scene statically via Engine singleton.
static func _get_tree() -> SceneTree:
return Engine.get_main_loop() as SceneTree
# Get autoloaded Gotm instance from scene's root.
static func _get_gotm() -> Node:
return _get_tree().root.get_node("Gotm")
# Simplify string somewhat. We want exact matches, but with some reasonable fuzziness.
static func _init_search_string_encoders() -> Array:
var encoders: Array = [
["[àáâãäå]", "a"],
["[èéêë]", "e"],
["[ìíîï]", "i"],
["[òóôõöő]", "o"],
["[ùúûüű]", "u"],
["[ýŷÿ]", "y"],
["ñ", "n"],
["[çc]", "k"],
["ß", "s"],
["[-/]", " "],
["[^a-z0-9 ]", ""],
["\\s+", " "],
["^\\s+", ""],
["\\s+$", ""]
]
for encoder in encoders:
var regex: RegEx = RegEx.new()
regex.compile(encoder[0])
encoder[0] = regex
return encoders
# Initialize socket for fetching lobbies on local network.
static func _init_socket() -> void:
var g = _get_gotm()
if not g._impl.sockets:
g._impl.sockets = []
var is_listening = false
for i in range(5):
var socket = PacketPeerUDP.new()
if socket.has_method("set_broadcast_enabled"):
socket.set_broadcast_enabled(true)
if not is_listening:
is_listening = socket.listen(8075 + i) == OK
socket.set_dest_address("255.255.255.255", 8075 + i)
g._impl.sockets.push_back(socket)
if not is_listening:
push_error("Failed to listen for lobbies. All ports 8075-8079 are busy.")
# Attach some global state to autoloaded Gotm instance.
static func _initialize(GotmLobbyT, GotmUserT) -> void:
var g = _get_gotm()
g._impl = {
"lobbies": [],
"chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-",
"rng": RandomNumberGenerator.new(),
"search_string_encoders": _init_search_string_encoders(),
"sockets": null,
"lobby_requests": {},
"GotmLobbyT": GotmLobbyT,
"GotmUserT": GotmUserT,
"is_listening": false
}
g._impl.rng.randomize()
g.user._impl.id = _generate_id()
g.user.address = "localhost"
static func _process() -> void:
var g = _get_gotm()
if g.lobby:
if OS.get_system_time_msecs() - g.lobby._impl.last_heartbeat > 2500:
_init_socket()
_put_sockets({
"op": "peer_heartbeat",
"data": {
"lobby_id": g.lobby.id,
"id": g.user._impl.id
}
})
g.lobby._impl.last_heartbeat = OS.get_system_time_msecs()
if g._impl.sockets:
for socket in g._impl.sockets:
while socket.get_available_packet_count() > 0:
var v = socket.get_var()
if v.op == "get_lobbies":
var data = null
if g.lobby and g.lobby.is_host():
data = {
"id": g.lobby.id,
"name": g.lobby.name,
"peers": [],
"invite_link": g.lobby.invite_link,
"_impl": g.lobby._impl
}
_put_sockets({"op": "lobby", "data": data, "id": v.id})
elif v.op == "leave_lobby":
if g.lobby and v.data.lobby_id == g.lobby.id:
if g.lobby.is_host():
_put_sockets({
"op": "peer_left",
"data": {
"lobby_id": g.lobby.id,
"id": v.data.id
}
})
elif v.data.id == g.lobby.host._impl.id:
_leave_lobby(g.lobby)
elif v.op == "join_lobby":
var data = null
if g.lobby and g.lobby.is_host() and v.data.lobby_id == g.lobby.id:
_put_sockets({
"op": "peer_joined",
"data": {
"lobby_id": g.lobby.id,
"address": socket.get_packet_ip(),
"id": v.data.id
},
"id": v.id
})
var peers = []
for peer in g.lobby.peers:
peers.push_back({"address": peer.address, "_impl": peer._impl})
data = {
"id": g.lobby.id,
"name": g.lobby.name,
"peers": peers,
"invite_link": g.lobby.invite_link,
"_impl": g.lobby._impl
}
_put_sockets({"op": "lobby", "data": data, "id": v.id})
elif v.op == "peer_left":
if g.lobby and v.data.lobby_id == g.lobby.id:
for peer in g.lobby.peers.duplicate():
if peer._impl.id == v.data.id:
g.lobby.peers.erase(peer)
g.lobby.emit_signal("peer_left", peer)
elif v.op == "peer_joined":
if g.lobby and v.data.lobby_id == g.lobby.id and not g._impl.lobby_requests.has(v.id):
var peer = g._impl.GotmUserT.new()
peer.address = v.data.address
peer._impl.id = v.data.id
g.lobby.peers.push_back(peer)
g.lobby.emit_signal("peer_joined", peer)
if g.lobby.is_host():
g.lobby._impl.heartbeats[v.data.id] = OS.get_system_time_msecs()
elif v.op == "peer_heartbeat":
if g.lobby and v.data.lobby_id == g.lobby.id:
g.lobby._impl.heartbeats[v.data.id] = OS.get_system_time_msecs()
elif v.op == "lobby":
if v.data and g._impl.lobby_requests.has(v.id):
var lobby = g._impl.GotmLobbyT.new()
var peers = []
if not v.data._impl.host_id.empty():
v.data.peers.push_back({
"address": socket.get_packet_ip(),
"_impl": {
"id": v.data._impl.host_id
}
})
for peer in v.data.peers:
var p = g._impl.GotmUserT.new()
p.address = peer.address
p._impl = peer._impl
peers.push_back(p)
lobby.hidden = false
lobby.locked = false
lobby.id = v.data.id
lobby.name = v.data.name
lobby.peers = peers
lobby.invite_link = v.data.invite_link
lobby._impl = v.data._impl
lobby._impl.address = socket.get_packet_ip()
lobby.me.address = "127.0.0.1"
lobby.host.address = socket.get_packet_ip()
lobby.host._impl.id = v.data._impl.host_id
g._impl.lobby_requests[v.id].push_back(lobby)
if g.lobby:
for peer_id in g.lobby._impl.heartbeats.duplicate():
if OS.get_system_time_msecs() - g.lobby._impl.heartbeats[peer_id] > 10000:
if g.lobby.is_host():
_put_sockets({
"op": "peer_left",
"data": {
"lobby_id": g.lobby.id,
"id": peer_id
}
})
g.lobby._impl.heartbeats.erase(peer_id)
elif peer_id == g.lobby.host._impl.id:
_leave_lobby(g.lobby)