This repository has been archived on 2020-09-30. You can view files and clone it, but cannot push or open issues or pull requests.
odyssey-old/gotm/_impl/_GotmImpl.gd

587 lines
16 KiB
GDScript

# 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)
break
# Improve search experience a little by adding fuzziness.
static func _encode_search_string(s: String) -> String:
s = s.to_lower()
var encoders: Array = _get_gotm()._impl.search_string_encoders
for encoder in encoders:
s = encoder[0].sub(s, encoder[1], true)
return s
# Return true if 'lobby' matches filter options in 'fetch'.
static func _match_lobby(lobby, fetch) -> bool:
if lobby.locked or lobby.hidden:
return false
if not fetch.filter_name.empty():
var name: String = _encode_search_string(lobby.name)
var query: String = _encode_search_string(fetch.filter_name)
if not query.empty() and name.find(query) < 0:
return false
var lobby_props: Dictionary = {}
for key in lobby._impl.filterable_props:
if not lobby._impl.props.has(key):
return false
if not fetch.filter_properties.has(key):
return false
var lhs = fetch.filter_properties[key]
var rhs = lobby._impl.props[key]
if lhs != null and lhs != rhs:
return false
return true
# Used to detect changes.
static func _stringify_fetch_state(fetch) -> String:
var d: Array = [
fetch.filter_name,
fetch.filter_properties,
fetch.sort_property,
fetch.sort_property,
fetch.sort_ascending,
fetch.sort_min,
fetch.sort_max,
fetch.sort_min_exclusive,
fetch.sort_max_exclusive
]
return JSON.print(d)
# Return sorted copy of 'lobbies' using sort options in 'fetch'.
static func _sort_lobbies(lobbies: Array, fetch) -> Array:
var sorted: Array = []
var g = _get_gotm()
for lobby in lobbies:
if fetch.sort_property.empty():
sorted.push_back(lobby)
var v = lobby._impl.props.get(fetch.sort_property)
if v == null:
continue
if fetch.sort_min != null:
if _GotmImplUtility.is_less(v, fetch.sort_min):
continue
if fetch.sort_min_exclusive and not _GotmImplUtility.is_greater(v, fetch.sort_min):
continue
if fetch.sort_max != null:
if _GotmImplUtility.is_greater(v, fetch.sort_max):
continue
if fetch.sort_max_exclusive and not _GotmImplUtility.is_less(v, fetch.sort_max):
continue
sorted.push_back(lobby)
var sorter: LobbySorter = LobbySorter.new()
sorter.fetch = fetch
sorter.g = g
sorted.sort_custom(sorter, "sort")
return sorted
static func _put_sockets(v: Dictionary):
_init_socket()
for socket in _get_gotm()._impl.sockets:
socket.put_var(v)
static func _request_lobbies() -> Array:
var g = _get_gotm()
var request_id: String = _generate_id()
g._impl.lobby_requests[request_id] = []
_put_sockets({"op": "get_lobbies", "id": request_id})
yield(g.get_tree().create_timer(0.5), "timeout")
var lobbies = g._impl.lobby_requests[request_id]
g._impl.lobby_requests.erase(request_id)
return lobbies
static func _request_join(lobby_id: String):
var g = _get_gotm()
var request_id: String = _generate_id()
g._impl.lobby_requests[request_id] = []
_put_sockets({
"op": "join_lobby",
"id": request_id,
"data": {
"lobby_id": lobby_id,
"id": g.user._impl.id
}
})
yield(g.get_tree().create_timer(0.5), "timeout")
var lobbies = g._impl.lobby_requests[request_id]
g._impl.lobby_requests.erase(request_id)
for lobby in lobbies:
if lobby:
return lobby
return null
static func _fetch_lobbies(fetch, count: int, type: String) -> Array:
var g = _get_gotm()
# Reset fetch state if user has modified any options.
var stringified_state: String = _stringify_fetch_state(fetch)
if not fetch._impl.has("last_state") or stringified_state != fetch._impl.last_state:
fetch._impl.last_state = stringified_state
fetch._impl.last_lobby = -1
fetch._impl.start_lobby = -1
# Apply filter options
var lobbies: Array = []
for lobby in yield(_request_lobbies(), "completed") + g._impl.lobbies:
if _match_lobby(lobby, fetch):
lobbies.push_back(lobby)
# Apply sort options
lobbies = _sort_lobbies(lobbies, fetch)
count = min(8, count)
var index: int = 0
if type == "first":
index = 0
elif type == "next":
index = fetch._impl.last_lobby + 1
elif type == "current":
index = fetch._impl.start_lobby + 1
elif type == "previous":
index = max(fetch._impl.start_lobby - count, 0)
# Get 'count' lobbies.
var result: Array = []
for i in range(index, min(index + count, lobbies.size())):
result.push_back(lobbies[i])
# Write down last lobby for subsequent 'next' calls.
if not result.empty():
var start: int = lobbies.find(result.front()) - 1
fetch._impl.start_lobby = max(start, -1)
fetch._impl.last_lobby = lobbies.find(result.back())
elif index > 0:
fetch._impl.start_lobby = fetch._impl.last_lobby
yield(_get_tree().create_timer(0.25), "timeout") # fake delay
return result
# Common initialization.
static func _add_lobby(lobby):
var g = _get_gotm()
lobby.id = _generate_id()
lobby.invite_link = "https://gotm.io/my-studio/my-game/"
lobby.invite_link += "?connectToken=" + _generate_id()
lobby._impl = {
# Not exposed to user, so doesn't have to be a real timestamp.
"created": OS.get_system_time_msecs(),
"props": {},
"sortable_props": [],
"filterable_props": [],
"heartbeats": {},
"last_heartbeat": 0,
"host_id": "",
"address": ""
}
lobby.me._impl.id = g.user._impl.id
g._impl.lobbies.push_back(lobby)
return lobby
static func _host_lobby(lobby):
var g = _get_gotm()
_leave_lobby(g.lobby)
lobby = _add_lobby(lobby)
lobby._impl.address = "127.0.0.1"
lobby.host.address = "127.0.0.1"
lobby._impl.host_id = g.user._impl.id
lobby.host._impl.id = g.user._impl.id
lobby.me.address = "127.0.0.1"
g.lobby = lobby
g.emit_signal("lobby_changed")
_init_socket()
return lobby
static func _join_lobby(lobby) -> bool:
var g = _get_gotm()
_leave_lobby(g.lobby)
if not g._impl.lobbies.has(lobby):
lobby = yield(_request_join(lobby.id), "completed")
else:
yield(g.get_tree().create_timer(0.25), "timeout")
if not lobby or lobby.locked:
return false
lobby.host.address = lobby._impl.address
lobby.host._impl.id = lobby._impl.host_id
lobby.me.address = "127.0.0.1"
g.lobby = lobby
g.emit_signal("lobby_changed")
return true
static func _is_lobby_host(lobby) -> bool:
var g = _get_gotm()
return lobby.host._impl.id == g.user._impl.id
static func _kick_lobby_peer(lobby, peer) -> bool:
var g = _get_gotm()
if not lobby.is_host():
return false
if g.user._impl.id == peer._impl.id:
_leave_lobby(lobby)
else:
for p in lobby.peers.duplicate():
if p._impl.id != peer._impl.id:
continue
lobby.peers.erase(p)
if lobby == g.lobby:
lobby.emit_signal("peer_left", p)
break
return true
static func _leave_lobby(lobby) -> void:
if not lobby:
return
var g = _get_gotm()
if g.lobby == lobby:
if lobby.host.address == lobby.me.address:
g._impl.lobbies.erase(lobby)
lobby.me.address = ""
lobby.host.address = ""
_put_sockets({
"op": "leave_lobby",
"data": {
"lobby_id": lobby.id,
"id": g.user._impl.id
}
})
g.lobby = null
g.emit_signal("lobby_changed")
static func _truncate_string(s: String) -> String:
return s.substr(0, 64)
static func _set_lobby_property(lobby, name: String, value) -> void:
name = _truncate_string(name)
if value == null:
lobby._impl.props.erase(name)
match typeof(value):
TYPE_BOOL:
pass
TYPE_INT:
pass
TYPE_REAL:
pass
TYPE_STRING:
value = _truncate_string(value)
_:
push_error("Invalid lobby property type.")
return
lobby._impl.props[name] = value
static func _get_lobby_property(lobby, name: String):
return lobby._impl.props[_truncate_string(name)]
static func _set_lobby_filterable(lobby, property_name: String, filterable: bool) -> void:
property_name = _truncate_string(property_name)
if not filterable:
lobby._impl.filterable_props.erase(property_name)
elif not lobby._impl.filterable_props.has(property_name):
lobby._impl.filterable_props.push_back(property_name)
static func _set_lobby_sortable(lobby, property_name: String, sortable: bool) -> void:
property_name = _truncate_string(property_name)
if not sortable:
lobby._impl.sortable_props.erase(property_name)
elif not lobby._impl.sortable_props.has(property_name):
lobby._impl.sortable_props.push_back(property_name)