mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-06 23:59:18 +08:00
Public party list should now display smoothly without hitches even for large server counts
This commit is contained in:
parent
012f2b31cf
commit
8c6a2c7e28
3
.idea/dictionaries/ericf.xml
generated
3
.idea/dictionaries/ericf.xml
generated
@ -323,6 +323,7 @@
|
|||||||
<w>chipfork</w>
|
<w>chipfork</w>
|
||||||
<w>chosenone</w>
|
<w>chosenone</w>
|
||||||
<w>chromebooks</w>
|
<w>chromebooks</w>
|
||||||
|
<w>chunksize</w>
|
||||||
<w>cjkcodecs</w>
|
<w>cjkcodecs</w>
|
||||||
<w>classmethod</w>
|
<w>classmethod</w>
|
||||||
<w>classmethods</w>
|
<w>classmethods</w>
|
||||||
@ -337,6 +338,7 @@
|
|||||||
<w>clionbin</w>
|
<w>clionbin</w>
|
||||||
<w>clioncode</w>
|
<w>clioncode</w>
|
||||||
<w>clionroot</w>
|
<w>clionroot</w>
|
||||||
|
<w>clipcount</w>
|
||||||
<w>cloudbuild</w>
|
<w>cloudbuild</w>
|
||||||
<w>cloudshell</w>
|
<w>cloudshell</w>
|
||||||
<w>cloudshellbuild</w>
|
<w>cloudshellbuild</w>
|
||||||
@ -713,6 +715,7 @@
|
|||||||
<w>firestore</w>
|
<w>firestore</w>
|
||||||
<w>firetv</w>
|
<w>firetv</w>
|
||||||
<w>firstline</w>
|
<w>firstline</w>
|
||||||
|
<w>firstpartykey</w>
|
||||||
<w>flac</w>
|
<w>flac</w>
|
||||||
<w>flagmat</w>
|
<w>flagmat</w>
|
||||||
<w>flagmaterial</w>
|
<w>flagmaterial</w>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
- Simplified ba.enum_by_value()
|
- Simplified ba.enum_by_value()
|
||||||
- Updated Google Play version to hopefully show friend high scores again on score screens (at least for levels that have an associated Google Play leaderboard).
|
- Updated Google Play version to hopefully show friend high scores again on score screens (at least for levels that have an associated Google Play leaderboard).
|
||||||
- Public-party-list now properly shows an error instead of 'loading...' when not signed in.
|
- Public-party-list now properly shows an error instead of 'loading...' when not signed in.
|
||||||
|
- Heavily reworked the public party list display code to be more efficient and avoid hitches even with large numbers of servers.
|
||||||
|
|
||||||
### 1.5.27 (20238)
|
### 1.5.27 (20238)
|
||||||
- Language functionality has been consolidated into a LanguageSubsystem object at ba.app.lang
|
- Language functionality has been consolidated into a LanguageSubsystem object at ba.app.lang
|
||||||
|
|||||||
@ -18,7 +18,6 @@ from ba._ads import AdsSubsystem
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import ba
|
import ba
|
||||||
from ba import _language
|
|
||||||
from bastd.actor import spazappearance
|
from bastd.actor import spazappearance
|
||||||
from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List
|
from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List
|
||||||
|
|
||||||
@ -193,7 +192,7 @@ class App:
|
|||||||
self.headless_mode: bool = self._env['headless_mode']
|
self.headless_mode: bool = self._env['headless_mode']
|
||||||
assert isinstance(self.headless_mode, bool)
|
assert isinstance(self.headless_mode, bool)
|
||||||
self.iircade_mode: bool = self._env['iircade_mode']
|
self.iircade_mode: bool = self._env['iircade_mode']
|
||||||
assert isinstance(self.headless_mode, bool)
|
assert isinstance(self.iircade_mode, bool)
|
||||||
self.allow_ticket_purchases: bool = not self.iircade_mode
|
self.allow_ticket_purchases: bool = not self.iircade_mode
|
||||||
|
|
||||||
# Misc.
|
# Misc.
|
||||||
@ -361,18 +360,17 @@ class App:
|
|||||||
# Run a test in a few seconds to see if we should pop up an existing
|
# Run a test in a few seconds to see if we should pop up an existing
|
||||||
# pending special offer.
|
# pending special offer.
|
||||||
def check_special_offer() -> None:
|
def check_special_offer() -> None:
|
||||||
from bastd.ui import specialoffer
|
from bastd.ui.specialoffer import show_offer
|
||||||
config = self.config
|
config = self.config
|
||||||
if ('pendingSpecialOffer' in config and _ba.get_public_login_id()
|
if ('pendingSpecialOffer' in config and _ba.get_public_login_id()
|
||||||
== config['pendingSpecialOffer']['a']):
|
== config['pendingSpecialOffer']['a']):
|
||||||
self.special_offer = config['pendingSpecialOffer']['o']
|
self.special_offer = config['pendingSpecialOffer']['o']
|
||||||
specialoffer.show_offer()
|
show_offer()
|
||||||
|
|
||||||
if not self.headless_mode:
|
if not self.headless_mode:
|
||||||
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
|
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
|
||||||
|
|
||||||
self.meta.on_app_launch()
|
self.meta.on_app_launch()
|
||||||
|
|
||||||
self.accounts.on_app_launch()
|
self.accounts.on_app_launch()
|
||||||
self.plugins.on_app_launch()
|
self.plugins.on_app_launch()
|
||||||
|
|
||||||
@ -395,7 +393,8 @@ class App:
|
|||||||
activity: Optional[ba.Activity] = _ba.get_foreground_host_activity()
|
activity: Optional[ba.Activity] = _ba.get_foreground_host_activity()
|
||||||
if (activity is not None and activity.allow_pausing
|
if (activity is not None and activity.allow_pausing
|
||||||
and not _ba.have_connected_clients()):
|
and not _ba.have_connected_clients()):
|
||||||
from ba import _gameutils, _language
|
from ba import _gameutils
|
||||||
|
from ba._language import Lstr
|
||||||
from ba._nodeactor import NodeActor
|
from ba._nodeactor import NodeActor
|
||||||
|
|
||||||
# FIXME: Shouldn't be touching scene stuff here;
|
# FIXME: Shouldn't be touching scene stuff here;
|
||||||
@ -408,18 +407,13 @@ class App:
|
|||||||
|
|
||||||
# FIXME: This should not be an attr on Actor.
|
# FIXME: This should not be an attr on Actor.
|
||||||
activity.paused_text = NodeActor(
|
activity.paused_text = NodeActor(
|
||||||
_ba.newnode(
|
_ba.newnode('text',
|
||||||
'text',
|
attrs={
|
||||||
attrs={
|
'text': Lstr(resource='pausedByHostText'),
|
||||||
'text':
|
'client_only': True,
|
||||||
_language.Lstr(resource='pausedByHostText'),
|
'flatness': 1.0,
|
||||||
'client_only':
|
'h_align': 'center'
|
||||||
True,
|
}))
|
||||||
'flatness':
|
|
||||||
1.0,
|
|
||||||
'h_align':
|
|
||||||
'center'
|
|
||||||
}))
|
|
||||||
|
|
||||||
def resume(self) -> None:
|
def resume(self) -> None:
|
||||||
"""Resume the game due to a user request or menu closing.
|
"""Resume the game due to a user request or menu closing.
|
||||||
|
|||||||
@ -22,6 +22,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
# Print a bit of info about pings, queries, etc.
|
# Print a bit of info about pings, queries, etc.
|
||||||
DEBUG_SERVER_COMMUNICATION = False
|
DEBUG_SERVER_COMMUNICATION = False
|
||||||
|
DEBUG_PROCESSING = False
|
||||||
|
|
||||||
|
|
||||||
class SubTabType(Enum):
|
class SubTabType(Enum):
|
||||||
@ -41,25 +42,151 @@ class PartyEntry:
|
|||||||
size: int = -1
|
size: int = -1
|
||||||
size_max: int = -1
|
size_max: int = -1
|
||||||
claimed: bool = False
|
claimed: bool = False
|
||||||
ping: Optional[int] = None
|
ping: Optional[float] = None
|
||||||
ping_interval: float = -1.0
|
ping_interval: float = -1.0
|
||||||
next_ping_time: float = -1.0
|
next_ping_time: float = -1.0
|
||||||
ping_attempts: int = 0
|
ping_attempts: int = 0
|
||||||
ping_responses: int = 0
|
ping_responses: int = 0
|
||||||
stats_addr: Optional[str] = None
|
stats_addr: Optional[str] = None
|
||||||
name_widget: Optional[ba.Widget] = None
|
clean_display_index: Optional[int] = None
|
||||||
ping_widget: Optional[ba.Widget] = None
|
|
||||||
stats_button: Optional[ba.Widget] = None
|
def get_key(self) -> str:
|
||||||
size_widget: Optional[ba.Widget] = None
|
"""Return the key used to store this party."""
|
||||||
|
return f'{self.address}_{self.port}'
|
||||||
|
|
||||||
|
|
||||||
|
class UIRow:
|
||||||
|
"""Wrangles UI for a row in the party list."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._name_widget: Optional[ba.Widget] = None
|
||||||
|
self._size_widget: Optional[ba.Widget] = None
|
||||||
|
self._ping_widget: Optional[ba.Widget] = None
|
||||||
|
self._stats_button: Optional[ba.Widget] = None
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
self._clear()
|
||||||
|
|
||||||
|
def _clear(self) -> None:
|
||||||
|
for widget in [
|
||||||
|
self._name_widget, self._size_widget, self._ping_widget,
|
||||||
|
self._stats_button
|
||||||
|
]:
|
||||||
|
if widget:
|
||||||
|
widget.delete()
|
||||||
|
|
||||||
|
def update(self, index: int, party: PartyEntry, sub_scroll_width: float,
|
||||||
|
sub_scroll_height: float, lineheight: float,
|
||||||
|
columnwidget: ba.Widget, join_text: ba.Widget,
|
||||||
|
filter_text: ba.Widget, existing_selection: Optional[Selection],
|
||||||
|
tab: PublicGatherTab) -> None:
|
||||||
|
"""Update for the given data."""
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
# Quick-out: if we've been marked clean for a certain index and
|
||||||
|
# we're still at that index, we're done.
|
||||||
|
if party.clean_display_index == index:
|
||||||
|
return
|
||||||
|
|
||||||
|
ping_good = _ba.get_account_misc_read_val('pingGood', 100)
|
||||||
|
ping_med = _ba.get_account_misc_read_val('pingMed', 500)
|
||||||
|
|
||||||
|
self._clear()
|
||||||
|
hpos = 20
|
||||||
|
vpos = sub_scroll_height - lineheight * index - 50
|
||||||
|
self._name_widget = ba.textwidget(
|
||||||
|
text=ba.Lstr(value=party.name),
|
||||||
|
parent=columnwidget,
|
||||||
|
size=(sub_scroll_width * 0.63, 20),
|
||||||
|
position=(0 + hpos, 4 + vpos),
|
||||||
|
selectable=True,
|
||||||
|
on_select_call=ba.WeakCall(
|
||||||
|
tab.set_public_party_selection,
|
||||||
|
Selection(party.get_key(), SelectionComponent.NAME)),
|
||||||
|
on_activate_call=ba.WeakCall(tab.on_public_party_activate, party),
|
||||||
|
click_activate=True,
|
||||||
|
maxwidth=sub_scroll_width * 0.45,
|
||||||
|
corner_scale=1.4,
|
||||||
|
autoselect=True,
|
||||||
|
color=(1, 1, 1, 0.3 if party.ping is None else 1.0),
|
||||||
|
h_align='left',
|
||||||
|
v_align='center')
|
||||||
|
ba.widget(edit=self._name_widget,
|
||||||
|
left_widget=join_text,
|
||||||
|
show_buffer_top=64.0,
|
||||||
|
show_buffer_bottom=64.0)
|
||||||
|
if existing_selection == Selection(party.get_key(),
|
||||||
|
SelectionComponent.NAME):
|
||||||
|
ba.containerwidget(edit=columnwidget,
|
||||||
|
selected_child=self._name_widget)
|
||||||
|
if party.stats_addr:
|
||||||
|
url = party.stats_addr.replace(
|
||||||
|
'${ACCOUNT}',
|
||||||
|
_ba.get_account_misc_read_val_2('resolvedAccountID',
|
||||||
|
'UNKNOWN'))
|
||||||
|
self._stats_button = ba.buttonwidget(
|
||||||
|
color=(0.3, 0.6, 0.94),
|
||||||
|
textcolor=(1.0, 1.0, 1.0),
|
||||||
|
label=ba.Lstr(resource='statsText'),
|
||||||
|
parent=columnwidget,
|
||||||
|
autoselect=True,
|
||||||
|
on_activate_call=ba.Call(ba.open_url, url),
|
||||||
|
on_select_call=ba.WeakCall(
|
||||||
|
tab.set_public_party_selection,
|
||||||
|
Selection(party.get_key(),
|
||||||
|
SelectionComponent.STATS_BUTTON)),
|
||||||
|
size=(120, 40),
|
||||||
|
position=(sub_scroll_width * 0.66 + hpos, 1 + vpos),
|
||||||
|
scale=0.9)
|
||||||
|
if existing_selection == Selection(
|
||||||
|
party.get_key(), SelectionComponent.STATS_BUTTON):
|
||||||
|
ba.containerwidget(edit=columnwidget,
|
||||||
|
selected_child=self._stats_button)
|
||||||
|
|
||||||
|
self._size_widget = ba.textwidget(
|
||||||
|
text=str(party.size) + '/' + str(party.size_max),
|
||||||
|
parent=columnwidget,
|
||||||
|
size=(0, 0),
|
||||||
|
position=(sub_scroll_width * 0.86 + hpos, 20 + vpos),
|
||||||
|
scale=0.7,
|
||||||
|
color=(0.8, 0.8, 0.8),
|
||||||
|
h_align='right',
|
||||||
|
v_align='center')
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
ba.widget(edit=self._name_widget, up_widget=filter_text)
|
||||||
|
if self._stats_button:
|
||||||
|
ba.widget(edit=self._stats_button, up_widget=filter_text)
|
||||||
|
|
||||||
|
self._ping_widget = ba.textwidget(parent=columnwidget,
|
||||||
|
size=(0, 0),
|
||||||
|
position=(sub_scroll_width * 0.94 +
|
||||||
|
hpos, 20 + vpos),
|
||||||
|
scale=0.7,
|
||||||
|
h_align='right',
|
||||||
|
v_align='center')
|
||||||
|
if party.ping is None:
|
||||||
|
ba.textwidget(edit=self._ping_widget,
|
||||||
|
text='-',
|
||||||
|
color=(0.5, 0.5, 0.5))
|
||||||
|
else:
|
||||||
|
ba.textwidget(edit=self._ping_widget,
|
||||||
|
text=str(int(party.ping)),
|
||||||
|
color=(0, 1, 0) if party.ping <= ping_good else
|
||||||
|
(1, 1, 0) if party.ping <= ping_med else (1, 0, 0))
|
||||||
|
|
||||||
|
party.clean_display_index = index
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class State:
|
class State:
|
||||||
"""State saved/restored only while the app is running."""
|
"""State saved/restored only while the app is running."""
|
||||||
sub_tab: SubTabType = SubTabType.JOIN
|
sub_tab: SubTabType = SubTabType.JOIN
|
||||||
parties: Optional[List[PartyEntry]] = None
|
parties: Optional[List[Tuple[str, PartyEntry]]] = None
|
||||||
next_entry_index: int = 0
|
next_entry_index: int = 0
|
||||||
filter_value: str = ''
|
filter_value: str = ''
|
||||||
|
have_server_list_response: bool = False
|
||||||
|
have_valid_server_list: bool = False
|
||||||
|
|
||||||
|
|
||||||
class SelectionComponent(Enum):
|
class SelectionComponent(Enum):
|
||||||
@ -71,7 +198,7 @@ class SelectionComponent(Enum):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Selection:
|
class Selection:
|
||||||
"""Describes the currently selected list element."""
|
"""Describes the currently selected list element."""
|
||||||
entry_index: int
|
entry_key: str
|
||||||
component: SelectionComponent
|
component: SelectionComponent
|
||||||
|
|
||||||
|
|
||||||
@ -104,7 +231,7 @@ class PingThread(threading.Thread):
|
|||||||
"""Thread for sending out game pings."""
|
"""Thread for sending out game pings."""
|
||||||
|
|
||||||
def __init__(self, address: str, port: int,
|
def __init__(self, address: str, port: int,
|
||||||
call: Callable[[str, int, Optional[int]], Optional[int]]):
|
call: Callable[[str, int, Optional[float]], Optional[int]]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._address = address
|
self._address = address
|
||||||
self._port = port
|
self._port = port
|
||||||
@ -141,7 +268,7 @@ class PingThread(threading.Thread):
|
|||||||
accessible = True
|
accessible = True
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
ping = int((time.time() - starttime) * 1000.0)
|
ping = (time.time() - starttime) * 1000.0
|
||||||
ba.pushcall(ba.Call(self._call, self._address, self._port,
|
ba.pushcall(ba.Call(self._call, self._address, self._port,
|
||||||
ping if accessible else None),
|
ping if accessible else None),
|
||||||
from_other_thread=True)
|
from_other_thread=True)
|
||||||
@ -213,13 +340,28 @@ class PublicGatherTab(GatherTab):
|
|||||||
self._host_max_party_size_minus_button: (Optional[ba.Widget]) = None
|
self._host_max_party_size_minus_button: (Optional[ba.Widget]) = None
|
||||||
self._host_max_party_size_plus_button: (Optional[ba.Widget]) = None
|
self._host_max_party_size_plus_button: (Optional[ba.Widget]) = None
|
||||||
self._host_status_text: Optional[ba.Widget] = None
|
self._host_status_text: Optional[ba.Widget] = None
|
||||||
|
self._signed_in = False
|
||||||
|
self._ui_rows: List[UIRow] = []
|
||||||
|
self._refresh_ui_row = 0
|
||||||
|
self._have_user_selected_row = False
|
||||||
|
self._first_valid_server_list_time: Optional[float] = None
|
||||||
|
|
||||||
|
# Parties indexed by id:
|
||||||
self._parties: Dict[str, PartyEntry] = {}
|
self._parties: Dict[str, PartyEntry] = {}
|
||||||
self._last_server_list_update_time: Optional[float] = None
|
|
||||||
self._first_server_list_rebuild_time: Optional[float] = None
|
# Parties sorted in display order:
|
||||||
|
self._parties_sorted: List[Tuple[str, PartyEntry]] = []
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
|
# Sorted parties with filter applied:
|
||||||
|
self._parties_displayed: Dict[str, PartyEntry] = {}
|
||||||
|
|
||||||
self._next_entry_index = 0
|
self._next_entry_index = 0
|
||||||
|
self._have_server_list_response = False
|
||||||
self._have_valid_server_list = False
|
self._have_valid_server_list = False
|
||||||
self._server_list_dirty = True
|
|
||||||
self._filter_value = ''
|
self._filter_value = ''
|
||||||
|
self._pending_party_infos: List[Dict[str, Any]] = []
|
||||||
|
self._last_sub_scroll_height = 0.0
|
||||||
|
|
||||||
def on_activate(
|
def on_activate(
|
||||||
self,
|
self,
|
||||||
@ -291,11 +433,10 @@ class PublicGatherTab(GatherTab):
|
|||||||
AddrFetchThread(ba.WeakCall(self._fetch_local_addr_cb)).start()
|
AddrFetchThread(ba.WeakCall(self._fetch_local_addr_cb)).start()
|
||||||
|
|
||||||
self._set_sub_tab(self._sub_tab, region_width, region_height)
|
self._set_sub_tab(self._sub_tab, region_width, region_height)
|
||||||
self._update_timer = ba.Timer(0.2,
|
self._update_timer = ba.Timer(0.1,
|
||||||
ba.WeakCall(self._update),
|
ba.WeakCall(self._update),
|
||||||
repeat=True,
|
repeat=True,
|
||||||
timetype=ba.TimeType.REAL)
|
timetype=ba.TimeType.REAL)
|
||||||
self._update()
|
|
||||||
return self._container
|
return self._container
|
||||||
|
|
||||||
def on_deactivate(self) -> None:
|
def on_deactivate(self) -> None:
|
||||||
@ -309,9 +450,11 @@ class PublicGatherTab(GatherTab):
|
|||||||
# server re-query or whatnot.
|
# server re-query or whatnot.
|
||||||
ba.app.ui.window_states[self.__class__.__name__] = State(
|
ba.app.ui.window_states[self.__class__.__name__] = State(
|
||||||
sub_tab=self._sub_tab,
|
sub_tab=self._sub_tab,
|
||||||
parties=[copy.copy(p) for p in self._get_ordered_parties()[:40]],
|
parties=[(i, copy.copy(p)) for i, p in self._parties_sorted[:40]],
|
||||||
next_entry_index=self._next_entry_index,
|
next_entry_index=self._next_entry_index,
|
||||||
filter_value=self._filter_value)
|
filter_value=self._filter_value,
|
||||||
|
have_server_list_response=self._have_server_list_response,
|
||||||
|
have_valid_server_list=self._have_valid_server_list)
|
||||||
|
|
||||||
def restore_state(self) -> None:
|
def restore_state(self) -> None:
|
||||||
state = ba.app.ui.window_states.get(self.__class__.__name__)
|
state = ba.app.ui.window_states.get(self.__class__.__name__)
|
||||||
@ -323,11 +466,17 @@ class PublicGatherTab(GatherTab):
|
|||||||
# Restore the parties we stored.
|
# Restore the parties we stored.
|
||||||
if state.parties:
|
if state.parties:
|
||||||
self._parties = {
|
self._parties = {
|
||||||
f'{p.address}_{p.port}': copy.copy(p)
|
key: copy.copy(party)
|
||||||
for p in state.parties
|
for key, party in state.parties
|
||||||
}
|
}
|
||||||
|
self._parties_sorted = list(self._parties.items())
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
self._next_entry_index = state.next_entry_index
|
self._next_entry_index = state.next_entry_index
|
||||||
self._have_valid_server_list = True
|
|
||||||
|
# FIXME: should save/restore these too?..
|
||||||
|
self._have_server_list_response = state.have_server_list_response
|
||||||
|
self._have_valid_server_list = state.have_valid_server_list
|
||||||
self._filter_value = state.filter_value
|
self._filter_value = state.filter_value
|
||||||
|
|
||||||
def _set_sub_tab(self,
|
def _set_sub_tab(self,
|
||||||
@ -339,11 +488,16 @@ class PublicGatherTab(GatherTab):
|
|||||||
if playsound:
|
if playsound:
|
||||||
ba.playsound(ba.getsound('click01'))
|
ba.playsound(ba.getsound('click01'))
|
||||||
|
|
||||||
# If we're switching in from elsewhere, reset our selection.
|
# Reset our selection.
|
||||||
# (prevents selecting something way down the list if we switched away
|
# (prevents selecting something way down the list if we switched away
|
||||||
# and came back)
|
# and came back)
|
||||||
if self._sub_tab != value:
|
self._selection = None
|
||||||
self._selection = None
|
self._have_user_selected_row = False
|
||||||
|
|
||||||
|
# Reset refresh to the top and make sure everything refreshes.
|
||||||
|
self._refresh_ui_row = 0
|
||||||
|
for party in self._parties.values():
|
||||||
|
party.clean_display_index = None
|
||||||
|
|
||||||
self._sub_tab = value
|
self._sub_tab = value
|
||||||
active_color = (0.6, 1.0, 0.6)
|
active_color = (0.6, 1.0, 0.6)
|
||||||
@ -362,26 +516,6 @@ class PublicGatherTab(GatherTab):
|
|||||||
|
|
||||||
if value is SubTabType.JOIN:
|
if value is SubTabType.JOIN:
|
||||||
self._build_join_tab(region_width, region_height)
|
self._build_join_tab(region_width, region_height)
|
||||||
self._server_list_dirty = True
|
|
||||||
|
|
||||||
# If we're not currently signed in, ignore any list we
|
|
||||||
# consider any list we previously retrieved.
|
|
||||||
if _ba.get_account_state() != 'signed_in':
|
|
||||||
self._have_valid_server_list = False
|
|
||||||
|
|
||||||
# If we've not yet successfully fetched a server list,
|
|
||||||
# force an attempt now and show the user a 'loading...' status.
|
|
||||||
if not self._have_valid_server_list:
|
|
||||||
self._last_server_list_query_time = None
|
|
||||||
join_status_str = ba.Lstr(
|
|
||||||
value='${A}...',
|
|
||||||
subs=[('${A}', ba.Lstr(resource='store.loadingText'))],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Otherwise we've got valid data already. Show it.
|
|
||||||
join_status_str = ba.Lstr(value='')
|
|
||||||
self._update_server_list()
|
|
||||||
ba.textwidget(edit=self._join_status_text, text=join_status_str)
|
|
||||||
|
|
||||||
if value is SubTabType.HOST:
|
if value is SubTabType.HOST:
|
||||||
self._build_host_tab(region_width, region_height)
|
self._build_host_tab(region_width, region_height)
|
||||||
@ -601,269 +735,87 @@ class PublicGatherTab(GatherTab):
|
|||||||
if _ba.get_public_party_enabled():
|
if _ba.get_public_party_enabled():
|
||||||
self._do_status_check()
|
self._do_status_check()
|
||||||
|
|
||||||
def _get_ordered_parties(self) -> List[PartyEntry]:
|
|
||||||
# Sort - show queue-enabled ones first and sort by lowest ping.
|
|
||||||
ordered_parties = sorted(
|
|
||||||
self._parties.values(),
|
|
||||||
key=lambda p: (
|
|
||||||
p.queue is None, # Show non-queued last.
|
|
||||||
p.ping if p.ping is not None else 999999,
|
|
||||||
p.index))
|
|
||||||
return ordered_parties
|
|
||||||
|
|
||||||
def _update_server_list(self) -> None:
|
|
||||||
cur_time = ba.time(ba.TimeType.REAL)
|
|
||||||
if self._first_server_list_rebuild_time is None:
|
|
||||||
self._first_server_list_rebuild_time = cur_time
|
|
||||||
|
|
||||||
# We get called quite often (for each ping response, etc) so we want
|
|
||||||
# to limit our rebuilds to keep the UI responsive.
|
|
||||||
# Let's update faster for the first few seconds,
|
|
||||||
# then ease off to keep the list from jumping around.
|
|
||||||
since_first = cur_time - self._first_server_list_rebuild_time
|
|
||||||
wait_time = (1.0 if since_first < 2.0 else
|
|
||||||
2.5 if since_first < 10.0 else 5.0)
|
|
||||||
if (not self._server_list_dirty
|
|
||||||
and self._last_server_list_update_time is not None
|
|
||||||
and cur_time - self._last_server_list_update_time < wait_time):
|
|
||||||
return
|
|
||||||
|
|
||||||
# If we somehow got here without the required UI being in place...
|
|
||||||
columnwidget = self._join_list_column
|
|
||||||
if not columnwidget:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._last_server_list_update_time = cur_time
|
|
||||||
self._server_list_dirty = False
|
|
||||||
|
|
||||||
with ba.Context('ui'):
|
|
||||||
|
|
||||||
# Now kill and recreate all widgets.
|
|
||||||
for widget in columnwidget.get_children():
|
|
||||||
widget.delete()
|
|
||||||
|
|
||||||
ordered_parties = self._get_ordered_parties()
|
|
||||||
|
|
||||||
# If we've got a filter, filter them.
|
|
||||||
if self._filter_value:
|
|
||||||
# Let's do case-insensitive searching.
|
|
||||||
filterval = self._filter_value.lower()
|
|
||||||
ordered_parties = [
|
|
||||||
p for p in ordered_parties if filterval in p.name.lower()
|
|
||||||
]
|
|
||||||
|
|
||||||
sub_scroll_width = 830
|
|
||||||
lineheight = 42
|
|
||||||
sub_scroll_height = lineheight * len(ordered_parties) + 50
|
|
||||||
ba.containerwidget(edit=columnwidget,
|
|
||||||
size=(sub_scroll_width, sub_scroll_height))
|
|
||||||
|
|
||||||
# Ew; this rebuilding generates deferred selection callbacks
|
|
||||||
# so we need to generated deferred ignore notices for ourself.
|
|
||||||
def refresh_on() -> None:
|
|
||||||
self._refreshing_list = True
|
|
||||||
|
|
||||||
ba.pushcall(refresh_on)
|
|
||||||
|
|
||||||
# Janky - allow escaping if there's nothing in us.
|
|
||||||
ba.containerwidget(edit=self._host_scrollwidget,
|
|
||||||
claims_up_down=(len(ordered_parties) > 0))
|
|
||||||
|
|
||||||
self._build_server_entry_lines(lineheight, ordered_parties,
|
|
||||||
sub_scroll_height, sub_scroll_width)
|
|
||||||
|
|
||||||
# So our selection callbacks can start firing..
|
|
||||||
def refresh_off() -> None:
|
|
||||||
self._refreshing_list = False
|
|
||||||
|
|
||||||
ba.pushcall(refresh_off)
|
|
||||||
|
|
||||||
def _build_server_entry_lines(self, lineheight: float,
|
|
||||||
ordered_parties: List[PartyEntry],
|
|
||||||
sub_scroll_height: float,
|
|
||||||
sub_scroll_width: float) -> None:
|
|
||||||
existing_selection = self._selection
|
|
||||||
columnwidget = self._join_list_column
|
|
||||||
first = True
|
|
||||||
assert columnwidget
|
|
||||||
ping_good = _ba.get_account_misc_read_val('pingGood', 100)
|
|
||||||
ping_med = _ba.get_account_misc_read_val('pingMed', 500)
|
|
||||||
for i, party in enumerate(ordered_parties):
|
|
||||||
hpos = 20
|
|
||||||
vpos = sub_scroll_height - lineheight * i - 50
|
|
||||||
party.name_widget = ba.textwidget(
|
|
||||||
text=ba.Lstr(value=party.name),
|
|
||||||
parent=columnwidget,
|
|
||||||
size=(sub_scroll_width * 0.63, 20),
|
|
||||||
position=(0 + hpos, 4 + vpos),
|
|
||||||
selectable=True,
|
|
||||||
on_select_call=ba.WeakCall(
|
|
||||||
self._set_public_party_selection,
|
|
||||||
Selection(party.index, SelectionComponent.NAME)),
|
|
||||||
on_activate_call=ba.WeakCall(self._on_public_party_activate,
|
|
||||||
party),
|
|
||||||
click_activate=True,
|
|
||||||
maxwidth=sub_scroll_width * 0.45,
|
|
||||||
corner_scale=1.4,
|
|
||||||
autoselect=True,
|
|
||||||
color=(1, 1, 1, 0.3 if party.ping is None else 1.0),
|
|
||||||
h_align='left',
|
|
||||||
v_align='center')
|
|
||||||
ba.widget(edit=party.name_widget,
|
|
||||||
left_widget=self._join_text,
|
|
||||||
show_buffer_top=64.0,
|
|
||||||
show_buffer_bottom=64.0)
|
|
||||||
if existing_selection == Selection(party.index,
|
|
||||||
SelectionComponent.NAME):
|
|
||||||
ba.containerwidget(edit=columnwidget,
|
|
||||||
selected_child=party.name_widget)
|
|
||||||
if party.stats_addr:
|
|
||||||
url = party.stats_addr.replace(
|
|
||||||
'${ACCOUNT}',
|
|
||||||
_ba.get_account_misc_read_val_2('resolvedAccountID',
|
|
||||||
'UNKNOWN'))
|
|
||||||
party.stats_button = ba.buttonwidget(
|
|
||||||
color=(0.3, 0.6, 0.94),
|
|
||||||
textcolor=(1.0, 1.0, 1.0),
|
|
||||||
label=ba.Lstr(resource='statsText'),
|
|
||||||
parent=columnwidget,
|
|
||||||
autoselect=True,
|
|
||||||
on_activate_call=ba.Call(ba.open_url, url),
|
|
||||||
on_select_call=ba.WeakCall(
|
|
||||||
self._set_public_party_selection,
|
|
||||||
Selection(party.index,
|
|
||||||
SelectionComponent.STATS_BUTTON)),
|
|
||||||
size=(120, 40),
|
|
||||||
position=(sub_scroll_width * 0.66 + hpos, 1 + vpos),
|
|
||||||
scale=0.9)
|
|
||||||
if existing_selection == Selection(
|
|
||||||
party.index, SelectionComponent.STATS_BUTTON):
|
|
||||||
ba.containerwidget(edit=columnwidget,
|
|
||||||
selected_child=party.stats_button)
|
|
||||||
else:
|
|
||||||
if party.stats_button:
|
|
||||||
party.stats_button.delete()
|
|
||||||
party.stats_button = None
|
|
||||||
|
|
||||||
if first:
|
|
||||||
if party.stats_button:
|
|
||||||
ba.widget(edit=party.stats_button,
|
|
||||||
up_widget=self._filter_text)
|
|
||||||
if party.name_widget:
|
|
||||||
ba.widget(edit=party.name_widget,
|
|
||||||
up_widget=self._filter_text)
|
|
||||||
first = False
|
|
||||||
|
|
||||||
party.size_widget = ba.textwidget(
|
|
||||||
text=str(party.size) + '/' + str(party.size_max),
|
|
||||||
parent=columnwidget,
|
|
||||||
size=(0, 0),
|
|
||||||
position=(sub_scroll_width * 0.86 + hpos, 20 + vpos),
|
|
||||||
scale=0.7,
|
|
||||||
color=(0.8, 0.8, 0.8),
|
|
||||||
h_align='right',
|
|
||||||
v_align='center')
|
|
||||||
party.ping_widget = ba.textwidget(
|
|
||||||
parent=columnwidget,
|
|
||||||
size=(0, 0),
|
|
||||||
position=(sub_scroll_width * 0.94 + hpos, 20 + vpos),
|
|
||||||
scale=0.7,
|
|
||||||
h_align='right',
|
|
||||||
v_align='center')
|
|
||||||
if party.ping is None:
|
|
||||||
ba.textwidget(edit=party.ping_widget,
|
|
||||||
text='-',
|
|
||||||
color=(0.5, 0.5, 0.5))
|
|
||||||
else:
|
|
||||||
ba.textwidget(edit=party.ping_widget,
|
|
||||||
text=str(party.ping),
|
|
||||||
color=(0, 1, 0) if party.ping <= ping_good else
|
|
||||||
(1, 1, 0) if party.ping <= ping_med else
|
|
||||||
(1, 0, 0))
|
|
||||||
|
|
||||||
def _on_public_party_query_result(
|
def _on_public_party_query_result(
|
||||||
self, result: Optional[Dict[str, Any]]) -> None:
|
self, result: Optional[Dict[str, Any]]) -> None:
|
||||||
# starttime = time.time()
|
starttime = time.time()
|
||||||
with ba.Context('ui'):
|
self._have_server_list_response = True
|
||||||
# Any time we get any result at all, kill our loading status.
|
|
||||||
status_text = self._join_status_text
|
|
||||||
if status_text:
|
|
||||||
# Don't show results if not signed in
|
|
||||||
# (probably didn't get any anyway).
|
|
||||||
if _ba.get_account_state() != 'signed_in':
|
|
||||||
ba.textwidget(edit=status_text,
|
|
||||||
text=ba.Lstr(resource='notSignedInText'))
|
|
||||||
else:
|
|
||||||
if result is None:
|
|
||||||
ba.textwidget(edit=status_text,
|
|
||||||
text=ba.Lstr(resource='errorText'))
|
|
||||||
else:
|
|
||||||
ba.textwidget(edit=status_text, text='')
|
|
||||||
|
|
||||||
if result is not None:
|
if result is None:
|
||||||
self._have_valid_server_list = True
|
self._have_valid_server_list = False
|
||||||
parties_in = result['l']
|
return
|
||||||
else:
|
|
||||||
self._have_valid_server_list = False
|
|
||||||
parties_in = []
|
|
||||||
|
|
||||||
for partyval in list(self._parties.values()):
|
if not self._have_valid_server_list:
|
||||||
partyval.claimed = False
|
self._first_valid_server_list_time = time.time()
|
||||||
|
|
||||||
for party_in in parties_in:
|
self._have_valid_server_list = True
|
||||||
addr = party_in['a']
|
parties_in = result['l']
|
||||||
assert isinstance(addr, str)
|
|
||||||
port = party_in['p']
|
|
||||||
assert isinstance(port, int)
|
|
||||||
party_key = f'{addr}_{port}'
|
|
||||||
party = self._parties.get(party_key)
|
|
||||||
if party is None:
|
|
||||||
# If this party is new to us, init it.
|
|
||||||
party = self._parties[party_key] = PartyEntry(
|
|
||||||
address=addr,
|
|
||||||
next_ping_time=ba.time(ba.TimeType.REAL) +
|
|
||||||
0.001 * party_in['pd'],
|
|
||||||
index=self._next_entry_index)
|
|
||||||
self._next_entry_index += 1
|
|
||||||
assert isinstance(party.address, str)
|
|
||||||
assert isinstance(party.next_ping_time, float)
|
|
||||||
|
|
||||||
# Now, new or not, update its values.
|
# parties_in.reverse()
|
||||||
party.queue = party_in.get('q')
|
|
||||||
assert isinstance(party.queue, (str, type(None)))
|
assert isinstance(parties_in, list)
|
||||||
party.port = port
|
self._pending_party_infos += parties_in
|
||||||
party.name = party_in['n']
|
|
||||||
assert isinstance(party.name, str)
|
# To avoid causing a stutter here, we do most processing of
|
||||||
party.size = party_in['s']
|
# these entries incrementally in our _update() method.
|
||||||
assert isinstance(party.size, int)
|
# The one thing we do here is prune parties not contained in
|
||||||
party.size_max = party_in['sm']
|
# this result.
|
||||||
assert isinstance(party.size_max, int)
|
for partyval in list(self._parties.values()):
|
||||||
|
partyval.claimed = False
|
||||||
|
for party_in in parties_in:
|
||||||
|
addr = party_in['a']
|
||||||
|
assert isinstance(addr, str)
|
||||||
|
port = party_in['p']
|
||||||
|
assert isinstance(port, int)
|
||||||
|
party_key = f'{addr}_{port}'
|
||||||
|
party = self._parties.get(party_key)
|
||||||
|
if party is not None:
|
||||||
party.claimed = True
|
party.claimed = True
|
||||||
# (server provides this in milliseconds; we use seconds)
|
self._parties = {
|
||||||
party.ping_interval = 0.001 * party_in['pi']
|
key: val
|
||||||
assert isinstance(party.ping_interval, float)
|
for key, val in list(self._parties.items()) if val.claimed
|
||||||
party.stats_addr = party_in['sa']
|
}
|
||||||
assert isinstance(party.stats_addr, (str, type(None)))
|
self._parties_sorted = [
|
||||||
|
p for p in self._parties_sorted if p[1].claimed
|
||||||
|
]
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
# Prune unclaimed party entries.
|
# self._update_server_list()
|
||||||
self._parties = {
|
if DEBUG_PROCESSING:
|
||||||
key: val
|
print(f'Handled public party query results in '
|
||||||
for key, val in list(self._parties.items()) if val.claimed
|
f'{time.time()-starttime:.5f}s.')
|
||||||
}
|
|
||||||
|
|
||||||
# Make sure we update the list immediately in response to this.
|
|
||||||
self._server_list_dirty = True
|
|
||||||
|
|
||||||
self._update_server_list()
|
|
||||||
# print('updated in {time.time()-starttime:.3f}')
|
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
"""Periodic updating."""
|
"""Periodic updating."""
|
||||||
|
|
||||||
# Special case: if a party-queue window is up, don't do any of this
|
# Special case: if a party-queue window is up, don't do any of this
|
||||||
# (keeps things smoother).
|
# (keeps things smoother).
|
||||||
if ba.app.ui.have_party_queue_window:
|
# if ba.app.ui.have_party_queue_window:
|
||||||
return
|
# return
|
||||||
|
|
||||||
|
if self._sub_tab is SubTabType.JOIN:
|
||||||
|
|
||||||
|
# Keep our filter-text up to date from the UI.
|
||||||
|
text = self._filter_text
|
||||||
|
if text:
|
||||||
|
filter_value = cast(str, ba.textwidget(query=text))
|
||||||
|
if filter_value != self._filter_value:
|
||||||
|
self._filter_value = filter_value
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
|
self._query_party_list_periodically()
|
||||||
|
self._ping_parties_periodically()
|
||||||
|
|
||||||
|
# If any new party infos have come in, apply some of them.
|
||||||
|
self._process_pending_party_infos()
|
||||||
|
|
||||||
|
# Anytime we sign in/out, make sure we refresh our list.
|
||||||
|
signed_in = _ba.get_account_state() == 'signed_in'
|
||||||
|
if self._signed_in != signed_in:
|
||||||
|
self._signed_in = signed_in
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
|
# Update sorting to account for ping updates, new parties, etc.
|
||||||
|
self._update_party_lists()
|
||||||
|
|
||||||
# If we've got a party-name text widget, keep its value plugged
|
# If we've got a party-name text widget, keep its value plugged
|
||||||
# into our public host name.
|
# into our public host name.
|
||||||
@ -872,20 +824,214 @@ class PublicGatherTab(GatherTab):
|
|||||||
name = cast(str, ba.textwidget(query=self._host_name_text))
|
name = cast(str, ba.textwidget(query=self._host_name_text))
|
||||||
_ba.set_public_party_name(name)
|
_ba.set_public_party_name(name)
|
||||||
|
|
||||||
if self._sub_tab is SubTabType.JOIN:
|
# Update status text.
|
||||||
|
status_text = self._join_status_text
|
||||||
|
if status_text:
|
||||||
|
if not signed_in:
|
||||||
|
ba.textwidget(edit=status_text,
|
||||||
|
text=ba.Lstr(resource='notSignedInText'))
|
||||||
|
else:
|
||||||
|
# If we have a valid list, show no status; just the list.
|
||||||
|
# Otherwise show either 'loading...' or 'error' depending
|
||||||
|
# on whether this is our first go-round.
|
||||||
|
if self._have_valid_server_list:
|
||||||
|
ba.textwidget(edit=status_text, text='')
|
||||||
|
else:
|
||||||
|
if self._have_server_list_response:
|
||||||
|
ba.textwidget(edit=status_text,
|
||||||
|
text=ba.Lstr(resource='errorText'))
|
||||||
|
else:
|
||||||
|
ba.textwidget(
|
||||||
|
edit=status_text,
|
||||||
|
text=ba.Lstr(
|
||||||
|
value='${A}...',
|
||||||
|
subs=[('${A}',
|
||||||
|
ba.Lstr(resource='store.loadingText'))],
|
||||||
|
))
|
||||||
|
|
||||||
# If our filter value has changed, refresh the list
|
self._update_party_rows()
|
||||||
# using the new one.
|
|
||||||
text = self._filter_text
|
|
||||||
if text:
|
|
||||||
filter_value = cast(str, ba.textwidget(query=text))
|
|
||||||
if filter_value != self._filter_value:
|
|
||||||
self._filter_value = filter_value
|
|
||||||
self._server_list_dirty = True
|
|
||||||
self._update_server_list()
|
|
||||||
|
|
||||||
self._query_party_list_periodically()
|
def _update_party_rows(self) -> None:
|
||||||
self._ping_parties_periodically()
|
columnwidget = self._join_list_column
|
||||||
|
if not columnwidget:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert self._join_text
|
||||||
|
assert self._filter_text
|
||||||
|
|
||||||
|
# Janky - allow escaping when there's nothing in our list.
|
||||||
|
assert self._host_scrollwidget
|
||||||
|
ba.containerwidget(edit=self._host_scrollwidget,
|
||||||
|
claims_up_down=(len(self._parties_displayed) > 0))
|
||||||
|
|
||||||
|
# Clip if we have more UI rows than parties to show.
|
||||||
|
clipcount = len(self._ui_rows) - len(self._parties_displayed)
|
||||||
|
if clipcount > 0:
|
||||||
|
clipcount = max(clipcount, 50)
|
||||||
|
self._ui_rows = self._ui_rows[:-clipcount]
|
||||||
|
|
||||||
|
# If we have no parties to show, we're done.
|
||||||
|
if not self._parties_displayed:
|
||||||
|
return
|
||||||
|
|
||||||
|
sub_scroll_width = 830
|
||||||
|
lineheight = 42
|
||||||
|
sub_scroll_height = lineheight * len(self._parties_displayed) + 50
|
||||||
|
ba.containerwidget(edit=columnwidget,
|
||||||
|
size=(sub_scroll_width, sub_scroll_height))
|
||||||
|
|
||||||
|
# Any time our height changes, reset the refresh back to the top
|
||||||
|
# so we don't see ugly empty spaces appearing during initial list
|
||||||
|
# filling.
|
||||||
|
if sub_scroll_height != self._last_sub_scroll_height:
|
||||||
|
self._refresh_ui_row = 0
|
||||||
|
self._last_sub_scroll_height = sub_scroll_height
|
||||||
|
|
||||||
|
# Also note that we need to redisplay everything since its pos
|
||||||
|
# will have changed.. :(
|
||||||
|
for party in self._parties.values():
|
||||||
|
party.clean_display_index = None
|
||||||
|
|
||||||
|
# Ew; this rebuilding generates deferred selection callbacks
|
||||||
|
# so we need to push deferred notices so we know to ignore them.
|
||||||
|
def refresh_on() -> None:
|
||||||
|
self._refreshing_list = True
|
||||||
|
|
||||||
|
ba.pushcall(refresh_on)
|
||||||
|
|
||||||
|
# Ok, now here's the deal: we want to avoid creating/updating this
|
||||||
|
# entire list at one time because it will lead to hitches. So we
|
||||||
|
# refresh individual rows quickly in a loop.
|
||||||
|
rowcount = min(12, len(self._parties_displayed))
|
||||||
|
|
||||||
|
party_vals_displayed = list(self._parties_displayed.values())
|
||||||
|
while rowcount > 0:
|
||||||
|
refresh_row = self._refresh_ui_row % len(self._parties_displayed)
|
||||||
|
if refresh_row >= len(self._ui_rows):
|
||||||
|
self._ui_rows.append(UIRow())
|
||||||
|
refresh_row = len(self._ui_rows) - 1
|
||||||
|
|
||||||
|
# For the first few seconds after getting our first server-list,
|
||||||
|
# refresh only the top section of the list; this allows the lowest
|
||||||
|
# ping servers to show up more quickly.
|
||||||
|
if self._first_valid_server_list_time is not None:
|
||||||
|
if time.time() - self._first_valid_server_list_time < 4.0:
|
||||||
|
if refresh_row > 40:
|
||||||
|
refresh_row = 0
|
||||||
|
|
||||||
|
self._ui_rows[refresh_row].update(
|
||||||
|
refresh_row,
|
||||||
|
party_vals_displayed[refresh_row],
|
||||||
|
sub_scroll_width=sub_scroll_width,
|
||||||
|
sub_scroll_height=sub_scroll_height,
|
||||||
|
lineheight=lineheight,
|
||||||
|
columnwidget=columnwidget,
|
||||||
|
join_text=self._join_text,
|
||||||
|
existing_selection=self._selection,
|
||||||
|
filter_text=self._filter_text,
|
||||||
|
tab=self)
|
||||||
|
self._refresh_ui_row = refresh_row + 1
|
||||||
|
rowcount -= 1
|
||||||
|
|
||||||
|
# So our selection callbacks can start firing..
|
||||||
|
def refresh_off() -> None:
|
||||||
|
self._refreshing_list = False
|
||||||
|
|
||||||
|
ba.pushcall(refresh_off)
|
||||||
|
|
||||||
|
def _process_pending_party_infos(self) -> None:
|
||||||
|
starttime = time.time()
|
||||||
|
|
||||||
|
# We want to do this in small enough pieces to not cause UI hitches.
|
||||||
|
chunksize = 30
|
||||||
|
parties_in = self._pending_party_infos[:chunksize]
|
||||||
|
self._pending_party_infos = self._pending_party_infos[chunksize:]
|
||||||
|
for party_in in parties_in:
|
||||||
|
addr = party_in['a']
|
||||||
|
assert isinstance(addr, str)
|
||||||
|
port = party_in['p']
|
||||||
|
assert isinstance(port, int)
|
||||||
|
party_key = f'{addr}_{port}'
|
||||||
|
party = self._parties.get(party_key)
|
||||||
|
if party is None:
|
||||||
|
# If this party is new to us, init it.
|
||||||
|
party = PartyEntry(address=addr,
|
||||||
|
next_ping_time=ba.time(ba.TimeType.REAL) +
|
||||||
|
0.001 * party_in['pd'],
|
||||||
|
index=self._next_entry_index)
|
||||||
|
self._parties[party_key] = party
|
||||||
|
self._parties_sorted.append((party_key, party))
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
self._next_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 = port
|
||||||
|
party.name = party_in['n']
|
||||||
|
assert isinstance(party.name, str)
|
||||||
|
party.size = party_in['s']
|
||||||
|
assert isinstance(party.size, int)
|
||||||
|
party.size_max = party_in['sm']
|
||||||
|
assert isinstance(party.size_max, int)
|
||||||
|
|
||||||
|
# Server provides this in milliseconds; we use seconds.
|
||||||
|
party.ping_interval = 0.001 * party_in['pi']
|
||||||
|
assert isinstance(party.ping_interval, float)
|
||||||
|
party.stats_addr = party_in['sa']
|
||||||
|
assert isinstance(party.stats_addr, (str, type(None)))
|
||||||
|
|
||||||
|
# Make sure the party's UI gets updated.
|
||||||
|
party.clean_display_index = None
|
||||||
|
|
||||||
|
if DEBUG_PROCESSING and parties_in:
|
||||||
|
print(f'Processed {len(parties_in)} raw party infos in'
|
||||||
|
f' {time.time()-starttime:.5f}s.')
|
||||||
|
|
||||||
|
def _update_party_lists(self) -> None:
|
||||||
|
if not self._party_lists_dirty:
|
||||||
|
return
|
||||||
|
starttime = time.time()
|
||||||
|
assert len(self._parties_sorted) == len(self._parties)
|
||||||
|
|
||||||
|
self._parties_sorted.sort(key=lambda p: (
|
||||||
|
p[1].queue is None, # Show non-queued last.
|
||||||
|
p[1].ping if p[1].ping is not None else 999999.0,
|
||||||
|
p[1].index))
|
||||||
|
|
||||||
|
# If signed out or errored, show no parties.
|
||||||
|
if (_ba.get_account_state() != 'signed_in'
|
||||||
|
or not self._have_valid_server_list):
|
||||||
|
self._parties_displayed = {}
|
||||||
|
else:
|
||||||
|
if self._filter_value:
|
||||||
|
filterval = self._filter_value.lower()
|
||||||
|
self._parties_displayed = {
|
||||||
|
k: v
|
||||||
|
for k, v in self._parties_sorted
|
||||||
|
if filterval in v.name.lower()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._parties_displayed = dict(self._parties_sorted)
|
||||||
|
|
||||||
|
# Any time our selection disappears from the displayed list, go back to
|
||||||
|
# auto-selecting the top entry.
|
||||||
|
if (self._selection is not None
|
||||||
|
and self._selection.entry_key not in self._parties_displayed):
|
||||||
|
self._have_user_selected_row = False
|
||||||
|
|
||||||
|
# Whenever the user hasn't selected something, keep the first visible
|
||||||
|
# row selected.
|
||||||
|
if not self._have_user_selected_row and self._parties_displayed:
|
||||||
|
firstpartykey = next(iter(self._parties_displayed))
|
||||||
|
self._selection = Selection(firstpartykey, SelectionComponent.NAME)
|
||||||
|
|
||||||
|
self._party_lists_dirty = False
|
||||||
|
if DEBUG_PROCESSING:
|
||||||
|
print(f'Sorted {len(self._parties_sorted)} parties in'
|
||||||
|
f' {time.time()-starttime:.5f}s.')
|
||||||
|
|
||||||
def _query_party_list_periodically(self) -> None:
|
def _query_party_list_periodically(self) -> None:
|
||||||
now = ba.time(ba.TimeType.REAL)
|
now = ba.time(ba.TimeType.REAL)
|
||||||
@ -943,7 +1089,7 @@ class PublicGatherTab(GatherTab):
|
|||||||
ba.WeakCall(self._ping_callback)).start()
|
ba.WeakCall(self._ping_callback)).start()
|
||||||
|
|
||||||
def _ping_callback(self, address: str, port: Optional[int],
|
def _ping_callback(self, address: str, port: Optional[int],
|
||||||
result: Optional[int]) -> None:
|
result: Optional[float]) -> None:
|
||||||
# Look for a widget corresponding to this target.
|
# Look for a widget corresponding to this target.
|
||||||
# If we find one, update our list.
|
# If we find one, update our list.
|
||||||
party_key = f'{address}_{port}'
|
party_key = f'{address}_{port}'
|
||||||
@ -958,11 +1104,14 @@ class PublicGatherTab(GatherTab):
|
|||||||
if (current_ping is not None and result is not None
|
if (current_ping is not None and result is not None
|
||||||
and result < 150):
|
and result < 150):
|
||||||
smoothing = 0.7
|
smoothing = 0.7
|
||||||
party.ping = int(smoothing * current_ping +
|
party.ping = (smoothing * current_ping +
|
||||||
(1.0 - smoothing) * result)
|
(1.0 - smoothing) * result)
|
||||||
else:
|
else:
|
||||||
party.ping = result
|
party.ping = result
|
||||||
self._update_server_list()
|
|
||||||
|
# Need to re-sort the list and update the row display.
|
||||||
|
party.clean_display_index = None
|
||||||
|
self._party_lists_dirty = True
|
||||||
|
|
||||||
def _fetch_local_addr_cb(self, val: str) -> None:
|
def _fetch_local_addr_cb(self, val: str) -> None:
|
||||||
self._local_address = str(val)
|
self._local_address = str(val)
|
||||||
@ -1075,7 +1224,8 @@ class PublicGatherTab(GatherTab):
|
|||||||
fallback_resource='gatherWindow.startAdvertisingText'),
|
fallback_resource='gatherWindow.startAdvertisingText'),
|
||||||
on_activate_call=self._on_start_advertizing_press)
|
on_activate_call=self._on_start_advertizing_press)
|
||||||
|
|
||||||
def _on_public_party_activate(self, party: PartyEntry) -> None:
|
def on_public_party_activate(self, party: PartyEntry) -> None:
|
||||||
|
"""Called when a party is clicked or otherwise activated."""
|
||||||
if party.queue is not None:
|
if party.queue is not None:
|
||||||
from bastd.ui.partyqueue import PartyQueueWindow
|
from bastd.ui.partyqueue import PartyQueueWindow
|
||||||
ba.playsound(ba.getsound('swish'))
|
ba.playsound(ba.getsound('swish'))
|
||||||
@ -1091,10 +1241,12 @@ class PublicGatherTab(GatherTab):
|
|||||||
_ba.connect_to_party(address, port=port)
|
_ba.connect_to_party(address, port=port)
|
||||||
self._last_connect_attempt_time = now
|
self._last_connect_attempt_time = now
|
||||||
|
|
||||||
def _set_public_party_selection(self, sel: Selection) -> None:
|
def set_public_party_selection(self, sel: Selection) -> None:
|
||||||
|
"""Set the sel."""
|
||||||
if self._refreshing_list:
|
if self._refreshing_list:
|
||||||
return
|
return
|
||||||
self._selection = sel
|
self._selection = sel
|
||||||
|
self._have_user_selected_row = True
|
||||||
|
|
||||||
def _on_max_public_party_size_minus_press(self) -> None:
|
def _on_max_public_party_size_minus_press(self) -> None:
|
||||||
val = _ba.get_public_party_max_size()
|
val = _ba.get_public_party_max_size()
|
||||||
|
|||||||
@ -139,11 +139,13 @@
|
|||||||
<w>chatmessage</w>
|
<w>chatmessage</w>
|
||||||
<w>checkboxwidget</w>
|
<w>checkboxwidget</w>
|
||||||
<w>chrono</w>
|
<w>chrono</w>
|
||||||
|
<w>chunksize</w>
|
||||||
<w>cjief</w>
|
<w>cjief</w>
|
||||||
<w>classdict</w>
|
<w>classdict</w>
|
||||||
<w>cleanupcheck</w>
|
<w>cleanupcheck</w>
|
||||||
<w>clientid</w>
|
<w>clientid</w>
|
||||||
<w>clientinfo</w>
|
<w>clientinfo</w>
|
||||||
|
<w>clipcount</w>
|
||||||
<w>cmath</w>
|
<w>cmath</w>
|
||||||
<w>cmds</w>
|
<w>cmds</w>
|
||||||
<w>cmdvals</w>
|
<w>cmdvals</w>
|
||||||
@ -290,6 +292,7 @@
|
|||||||
<w>fifteenbits</w>
|
<w>fifteenbits</w>
|
||||||
<w>filterval</w>
|
<w>filterval</w>
|
||||||
<w>finishedptr</w>
|
<w>finishedptr</w>
|
||||||
|
<w>firstpartykey</w>
|
||||||
<w>fjco</w>
|
<w>fjco</w>
|
||||||
<w>fjcoiwef</w>
|
<w>fjcoiwef</w>
|
||||||
<w>flipbit</w>
|
<w>flipbit</w>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||||
<h4><em>last updated on 2020-11-08 for Ballistica version 1.5.28 build 20245</em></h4>
|
<h4><em>last updated on 2020-11-10 for Ballistica version 1.5.28 build 20245</em></h4>
|
||||||
<p>This page documents the Python classes and functions in the 'ba' module,
|
<p>This page documents the Python classes and functions in the 'ba' module,
|
||||||
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user