mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-22 23:15:49 +08:00
Fixed an issue where public party selection could jump around erratically between servers on the same IP addr
This commit is contained in:
parent
aef7a1fa6f
commit
2b59186d91
@ -52,6 +52,19 @@ class PartyEntry:
|
||||
size_widget: Optional[ba.Widget] = None
|
||||
|
||||
|
||||
class SelectionComponent(Enum):
|
||||
"""Describes what part of an entry is selected."""
|
||||
NAME = 'name'
|
||||
STATS_BUTTON = 'stats_button'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Selection:
|
||||
"""Describes the currently selected list element."""
|
||||
entry_index: int
|
||||
component: SelectionComponent
|
||||
|
||||
|
||||
class AddrFetchThread(threading.Thread):
|
||||
"""Thread for fetching an address in the bg."""
|
||||
|
||||
@ -77,6 +90,86 @@ class AddrFetchThread(threading.Thread):
|
||||
ba.print_exception()
|
||||
|
||||
|
||||
class PingThread(threading.Thread):
|
||||
"""Thread for sending out game pings."""
|
||||
|
||||
def __init__(self, address: str, port: int,
|
||||
call: Callable[[str, int, Optional[int]], Optional[int]]):
|
||||
super().__init__()
|
||||
self._address = address
|
||||
self._port = port
|
||||
self._call = call
|
||||
|
||||
def run(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
ba.app.ping_thread_count += 1
|
||||
try:
|
||||
import socket
|
||||
from ba.internal import get_ip_address_type
|
||||
socket_type = get_ip_address_type(self._address)
|
||||
sock = socket.socket(socket_type, socket.SOCK_DGRAM)
|
||||
sock.connect((self._address, self._port))
|
||||
|
||||
accessible = False
|
||||
starttime = time.time()
|
||||
|
||||
# Send a few pings and wait a second for
|
||||
# a response.
|
||||
sock.settimeout(1)
|
||||
for _i in range(3):
|
||||
sock.send(b'\x0b')
|
||||
result: Optional[bytes]
|
||||
try:
|
||||
# 11: BA_PACKET_SIMPLE_PING
|
||||
result = sock.recv(10)
|
||||
except Exception:
|
||||
result = None
|
||||
if result == b'\x0c':
|
||||
# 12: BA_PACKET_SIMPLE_PONG
|
||||
accessible = True
|
||||
break
|
||||
time.sleep(1)
|
||||
sock.close()
|
||||
ping = int((time.time() - starttime) * 1000.0)
|
||||
ba.pushcall(ba.Call(self._call, self._address, self._port,
|
||||
ping if accessible else None),
|
||||
from_other_thread=True)
|
||||
except ConnectionRefusedError:
|
||||
# Fine, server; sorry we pinged you. Hmph.
|
||||
pass
|
||||
except OSError as exc:
|
||||
import errno
|
||||
|
||||
# Ignore harmless errors.
|
||||
if exc.errno in {
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL,
|
||||
errno.EPERM, errno.EACCES
|
||||
}:
|
||||
pass
|
||||
elif exc.errno == 10022:
|
||||
# Windows 'invalid argument' error.
|
||||
pass
|
||||
elif exc.errno == 10051:
|
||||
# Windows 'a socket operation was attempted
|
||||
# to an unreachable network' error.
|
||||
pass
|
||||
elif exc.errno == errno.EADDRNOTAVAIL:
|
||||
if self._port == 0:
|
||||
# This has happened. Ignore.
|
||||
pass
|
||||
elif ba.do_once():
|
||||
print(f'Got EADDRNOTAVAIL on gather ping'
|
||||
f' for addr {self._address}'
|
||||
f' port {self._port}.')
|
||||
else:
|
||||
ba.print_exception(
|
||||
f'Error on gather ping '
|
||||
f'(errno={exc.errno})', once=True)
|
||||
except Exception:
|
||||
ba.print_exception('Error on gather ping', once=True)
|
||||
ba.app.ping_thread_count -= 1
|
||||
|
||||
|
||||
class PublicGatherTab(GatherTab):
|
||||
"""The public tab in the gather UI"""
|
||||
|
||||
@ -88,8 +181,8 @@ class PublicGatherTab(GatherTab):
|
||||
self._local_address: Optional[str] = None
|
||||
self._last_public_party_connect_attempt_time: Optional[float] = None
|
||||
self._sub_tab: SubTabType = SubTabType.JOIN
|
||||
self._public_party_list_selection: Optional[Tuple[str, str]] = None
|
||||
self._refreshing_public_party_list: Optional[bool] = None
|
||||
self._selection: Optional[Selection] = None
|
||||
self._refreshing_public_party_list = False
|
||||
self._update_timer: Optional[ba.Timer] = None
|
||||
self._host_scrollwidget: Optional[ba.Widget] = None
|
||||
self._host_name_text: Optional[ba.Widget] = None
|
||||
@ -137,12 +230,14 @@ class PublicGatherTab(GatherTab):
|
||||
click_activate=True,
|
||||
selectable=True,
|
||||
autoselect=True,
|
||||
on_activate_call=lambda: self._set_sub_tab(SubTabType.JOIN,
|
||||
region_width,
|
||||
region_height,
|
||||
region_left,
|
||||
region_bottom,
|
||||
playsound=True),
|
||||
on_activate_call=lambda: self._set_sub_tab(
|
||||
SubTabType.JOIN,
|
||||
region_width,
|
||||
region_height,
|
||||
region_left,
|
||||
region_bottom,
|
||||
playsound=True,
|
||||
),
|
||||
text=ba.Lstr(resource='gatherWindow.'
|
||||
'joinPublicPartyDescriptionText'))
|
||||
self._host_text = ba.textwidget(
|
||||
@ -157,12 +252,14 @@ class PublicGatherTab(GatherTab):
|
||||
click_activate=True,
|
||||
selectable=True,
|
||||
autoselect=True,
|
||||
on_activate_call=lambda: self._set_sub_tab(SubTabType.HOST,
|
||||
region_width,
|
||||
region_height,
|
||||
region_left,
|
||||
region_bottom,
|
||||
playsound=True),
|
||||
on_activate_call=lambda: self._set_sub_tab(
|
||||
SubTabType.HOST,
|
||||
region_width,
|
||||
region_height,
|
||||
region_left,
|
||||
region_bottom,
|
||||
playsound=True,
|
||||
),
|
||||
text=ba.Lstr(resource='gatherWindow.'
|
||||
'hostPublicPartyDescriptionText'))
|
||||
ba.widget(edit=self._join_text, up_widget=tab_button)
|
||||
@ -220,7 +317,7 @@ class PublicGatherTab(GatherTab):
|
||||
# (prevents selecting something way down the list if we switched away
|
||||
# and came back)
|
||||
if self._sub_tab != value:
|
||||
self._public_party_list_selection = None
|
||||
self._selection = None
|
||||
|
||||
self._sub_tab = value
|
||||
active_color = (0.6, 1.0, 0.6)
|
||||
@ -272,8 +369,7 @@ class PublicGatherTab(GatherTab):
|
||||
shadow=0.0,
|
||||
h_align='center',
|
||||
v_align='center')
|
||||
ba.textwidget(text=ba.Lstr(resource='gatherWindow.'
|
||||
'partySizeText'),
|
||||
ba.textwidget(text=ba.Lstr(resource='gatherWindow.partySizeText'),
|
||||
parent=self._container,
|
||||
size=(0, 0),
|
||||
position=(755, v - 8),
|
||||
@ -284,8 +380,7 @@ class PublicGatherTab(GatherTab):
|
||||
shadow=0.0,
|
||||
h_align='center',
|
||||
v_align='center')
|
||||
ba.textwidget(text=ba.Lstr(resource='gatherWindow.'
|
||||
'pingText'),
|
||||
ba.textwidget(text=ba.Lstr(resource='gatherWindow.pingText'),
|
||||
parent=self._container,
|
||||
size=(0, 0),
|
||||
position=(825, v - 8),
|
||||
@ -432,18 +527,18 @@ class PublicGatherTab(GatherTab):
|
||||
color=(0.6, 0.6, 0.6),
|
||||
position=(c_width * 0.5, v))
|
||||
v -= 90
|
||||
ba.textwidget(parent=self._container,
|
||||
text=ba.Lstr(resource='gatherWindow.'
|
||||
'dedicatedServerInfoText'),
|
||||
size=(0, 0),
|
||||
scale=0.7,
|
||||
flatness=1.0,
|
||||
shadow=0.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
maxwidth=c_width * 0.9,
|
||||
color=ba.app.ui.infotextcolor,
|
||||
position=(c_width * 0.5, v))
|
||||
ba.textwidget(
|
||||
parent=self._container,
|
||||
text=ba.Lstr(resource='gatherWindow.dedicatedServerInfoText'),
|
||||
size=(0, 0),
|
||||
scale=0.7,
|
||||
flatness=1.0,
|
||||
shadow=0.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
maxwidth=c_width * 0.9,
|
||||
color=ba.app.ui.infotextcolor,
|
||||
position=(c_width * 0.5, v))
|
||||
|
||||
# If public sharing is already on,
|
||||
# launch a status-check immediately.
|
||||
@ -487,7 +582,7 @@ class PublicGatherTab(GatherTab):
|
||||
p.queue is None, # Show non-queued last.
|
||||
p.ping if p.ping is not None else 999999,
|
||||
p.index))
|
||||
existing_selection = self._public_party_list_selection
|
||||
existing_selection = self._selection
|
||||
first = True
|
||||
|
||||
sub_scroll_width = 830
|
||||
@ -518,7 +613,7 @@ class PublicGatherTab(GatherTab):
|
||||
selectable=True,
|
||||
on_select_call=ba.WeakCall(
|
||||
self._set_public_party_selection,
|
||||
(party.address, 'name')),
|
||||
Selection(party.index, SelectionComponent.NAME)),
|
||||
on_activate_call=ba.WeakCall(
|
||||
self._on_public_party_activate, party),
|
||||
click_activate=True,
|
||||
@ -532,7 +627,9 @@ class PublicGatherTab(GatherTab):
|
||||
left_widget=self._join_text,
|
||||
show_buffer_top=64.0,
|
||||
show_buffer_bottom=64.0)
|
||||
if existing_selection == (party.address, 'name'):
|
||||
# if existing_selection == (party.address, 'name'):
|
||||
if existing_selection == Selection(party.index,
|
||||
SelectionComponent.NAME):
|
||||
ba.containerwidget(edit=columnwidget,
|
||||
selected_child=party.name_widget)
|
||||
if party.stats_addr:
|
||||
@ -549,11 +646,13 @@ class PublicGatherTab(GatherTab):
|
||||
on_activate_call=ba.Call(ba.open_url, url),
|
||||
on_select_call=ba.WeakCall(
|
||||
self._set_public_party_selection,
|
||||
(party.address, 'stats_button')),
|
||||
Selection(party.index,
|
||||
SelectionComponent.STATS_BUTTON)),
|
||||
size=(120, 40),
|
||||
position=(sub_scroll_width * 0.66 + hpos, 1 + vpos),
|
||||
scale=0.9)
|
||||
if existing_selection == (party.address, 'stats_button'):
|
||||
if existing_selection == Selection(
|
||||
party.index, SelectionComponent.STATS_BUTTON):
|
||||
ba.containerwidget(edit=columnwidget,
|
||||
selected_child=party.stats_button)
|
||||
else:
|
||||
@ -633,27 +732,28 @@ class PublicGatherTab(GatherTab):
|
||||
partyval.claimed = False
|
||||
|
||||
for party_in in parties_in:
|
||||
# Party is indexed by (ADDR)_(PORT)
|
||||
party_key = party_in['a'] + '_' + str(party_in['p'])
|
||||
addr = party_in['a']
|
||||
assert isinstance(addr, str)
|
||||
port = party_in['p']
|
||||
assert isinstance(port, int)
|
||||
party_key = f'{addr}_{port}'
|
||||
party = self._public_parties.get(party_key)
|
||||
if party is None:
|
||||
# If this party is new to us, init it.
|
||||
index = self._next_public_party_entry_index
|
||||
self._next_public_party_entry_index = index + 1
|
||||
party = self._public_parties[party_key] = PartyEntry(
|
||||
address=party_in.get('a'),
|
||||
address=addr,
|
||||
next_ping_time=ba.time(ba.TimeType.REAL) +
|
||||
0.001 * party_in['pd'],
|
||||
ping=None,
|
||||
index=index)
|
||||
index=self._next_public_party_entry_index)
|
||||
self._next_public_party_entry_index += 1
|
||||
assert isinstance(party.address, str)
|
||||
assert isinstance(party.next_ping_time, float)
|
||||
|
||||
# Now, new or not, update its values.
|
||||
party.queue = party_in.get('q')
|
||||
assert isinstance(party.queue, (str, type(None)))
|
||||
party.port = party_in.get('p')
|
||||
assert isinstance(party.port, int)
|
||||
party.port = port
|
||||
party.name = party_in['n']
|
||||
assert isinstance(party.name, str)
|
||||
party.size = party_in['s']
|
||||
@ -673,11 +773,9 @@ class PublicGatherTab(GatherTab):
|
||||
for key, val in list(self._public_parties.items())
|
||||
if val.claimed
|
||||
}
|
||||
|
||||
self._rebuild_public_party_list()
|
||||
|
||||
def _update_sub_tab(self) -> None:
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
# Special case: if a party-queue window is up, don't do any of this
|
||||
# (keeps things smoother).
|
||||
@ -723,92 +821,6 @@ class PublicGatherTab(GatherTab):
|
||||
5 if party.ping > 150 else 2)
|
||||
party.next_ping_time += party.ping_interval * mult
|
||||
|
||||
class PingThread(threading.Thread):
|
||||
"""Thread for sending out pings."""
|
||||
|
||||
def __init__(self, address: str, port: int,
|
||||
call: Callable[[str, int, Optional[int]],
|
||||
Optional[int]]):
|
||||
super().__init__()
|
||||
self._address = address
|
||||
self._port = port
|
||||
self._call = call
|
||||
|
||||
def run(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
ba.app.ping_thread_count += 1
|
||||
try:
|
||||
import socket
|
||||
from ba.internal import get_ip_address_type
|
||||
socket_type = get_ip_address_type(
|
||||
self._address)
|
||||
sock = socket.socket(socket_type,
|
||||
socket.SOCK_DGRAM)
|
||||
sock.connect((self._address, self._port))
|
||||
|
||||
accessible = False
|
||||
starttime = time.time()
|
||||
|
||||
# Send a few pings and wait a second for
|
||||
# a response.
|
||||
sock.settimeout(1)
|
||||
for _i in range(3):
|
||||
sock.send(b'\x0b')
|
||||
result: Optional[bytes]
|
||||
try:
|
||||
# 11: BA_PACKET_SIMPLE_PING
|
||||
result = sock.recv(10)
|
||||
except Exception:
|
||||
result = None
|
||||
if result == b'\x0c':
|
||||
# 12: BA_PACKET_SIMPLE_PONG
|
||||
accessible = True
|
||||
break
|
||||
time.sleep(1)
|
||||
sock.close()
|
||||
ping = int((time.time() - starttime) * 1000.0)
|
||||
ba.pushcall(ba.Call(
|
||||
self._call, self._address, self._port,
|
||||
ping if accessible else None),
|
||||
from_other_thread=True)
|
||||
except ConnectionRefusedError:
|
||||
# Fine, server; sorry we pinged you. Hmph.
|
||||
pass
|
||||
except OSError as exc:
|
||||
import errno
|
||||
|
||||
# Ignore harmless errors.
|
||||
if exc.errno in {
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EINVAL, errno.EPERM, errno.EACCES
|
||||
}:
|
||||
pass
|
||||
elif exc.errno == 10022:
|
||||
# Windows 'invalid argument' error.
|
||||
pass
|
||||
elif exc.errno == 10051:
|
||||
# Windows 'a socket operation was attempted
|
||||
# to an unreachable network' error.
|
||||
pass
|
||||
elif exc.errno == errno.EADDRNOTAVAIL:
|
||||
if self._port == 0:
|
||||
# This has happened. Ignore.
|
||||
pass
|
||||
elif ba.do_once():
|
||||
print(
|
||||
f'Got EADDRNOTAVAIL on gather ping'
|
||||
f' for addr {self._address}'
|
||||
f' port {self._port}.')
|
||||
else:
|
||||
ba.print_exception(
|
||||
f'Error on gather ping '
|
||||
f'(errno={exc.errno})',
|
||||
once=True)
|
||||
except Exception:
|
||||
ba.print_exception('Error on gather ping',
|
||||
once=True)
|
||||
ba.app.ping_thread_count -= 1
|
||||
|
||||
PingThread(party.address, party.port,
|
||||
ba.WeakCall(self._ping_callback)).start()
|
||||
|
||||
@ -831,8 +843,6 @@ class PublicGatherTab(GatherTab):
|
||||
|
||||
# This can happen if we switch away and then back to the
|
||||
# client tab while pings are in flight.
|
||||
# if 'ping_widget' not in party:
|
||||
# pass
|
||||
if party.ping_widget:
|
||||
self._rebuild_public_party_list()
|
||||
|
||||
@ -963,10 +973,10 @@ class PublicGatherTab(GatherTab):
|
||||
_ba.connect_to_party(address, port=port)
|
||||
self._last_public_party_connect_attempt_time = now
|
||||
|
||||
def _set_public_party_selection(self, sel: Tuple[str, str]) -> None:
|
||||
def _set_public_party_selection(self, sel: Selection) -> None:
|
||||
if self._refreshing_public_party_list:
|
||||
return
|
||||
self._public_party_list_selection = sel
|
||||
self._selection = sel
|
||||
|
||||
def _on_max_public_party_size_minus_press(self) -> None:
|
||||
val = _ba.get_public_party_max_size()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user