Skip to content

Instantly share code, notes, and snippets.

@gsmj
Last active January 25, 2025 22:58
Show Gist options
  • Save gsmj/e16a8b1e317ebb34c04c8cce4181de34 to your computer and use it in GitHub Desktop.
Save gsmj/e16a8b1e317ebb34c04c8cce4181de34 to your computer and use it in GitHub Desktop.
Rewritten eselection.py module in PySAMP 2.1 style. Original: https://gist.github.com/Cheaterman/4b54af8eb62ff94b79af3b8ecf8fc7f1
from dataclasses import dataclass, field
from typing import Callable
from pysamp import (
cancel_select_text_draw,
get_tick_count,
select_text_draw,
)
from pysamp.textdraw import TextDraw
from pysamp.player import Player
from pysamp.playertextdraw import PlayerTextDraw
from samp import INVALID_TEXT_DRAW # type: ignore
ITEMS_PER_PAGE = 18
_static_graphics = None
_player_menus: dict[int, "PlayerMenu"] = {}
class StaticGraphics:
"""Contains static graphics elements (background, etc) textdraws."""
def __init__(self):
self.background = TextDraw.create(
531.333374,
140.877777,
'_'
)
self.background.background_color(0)
self.background.alignment(1)
self.background.font(0)
self.background.letter_size(0.000000, 22.912965)
self.background.color(0)
self.background.set_outline(0)
self.background.set_proportional(True)
self.background.set_shadow(0)
self.background.use_box(True)
self.background.box_color(0x000000DD)
self.background.text_size(121.333328, 0.000000)
self.background.set_selectable(False)
self.right_arrow = TextDraw.create(
521.333374,
339.318542,
'LD_BEAT:right'
)
self.right_arrow.letter_size(0.000000, 0.000000)
self.right_arrow.text_size(5.999938, 7.051818)
self.right_arrow.alignment(1)
self.right_arrow.color(0xC0C0C0FF)
self.right_arrow.set_shadow(0)
self.right_arrow.set_outline(0)
self.right_arrow.font(4)
self.right_arrow.set_selectable(True)
self.left_arrow = TextDraw.create(
507.000305,
339.074066,
'LD_BEAT:left'
)
self.left_arrow.letter_size(0.000000, 0.000000)
self.left_arrow.text_size(5.999938, 7.051818)
self.left_arrow.alignment(1)
self.left_arrow.color(0xC0C0C0FF)
self.left_arrow.set_shadow(0)
self.left_arrow.set_outline(0)
self.left_arrow.font(4)
self.left_arrow.set_selectable(True)
self.top_banner = TextDraw.create(
531.000244,
155.811111,
'TopBanner'
)
self.top_banner.letter_size(0.000000, -0.447120)
self.top_banner.text_size(121.333328, 0.000000)
self.top_banner.alignment(1)
self.top_banner.color(0)
self.top_banner.use_box(True)
self.top_banner.box_color(0x808080FF)
self.top_banner.set_shadow(0)
self.top_banner.set_outline(0)
self.top_banner.font(0)
self.bottom_banner = TextDraw.create(
531.333618,
338.500305,
'BottomBanner'
)
self.bottom_banner.letter_size(0.000000, -0.447120)
self.bottom_banner.text_size(120.666656, 0.000000)
self.bottom_banner.alignment(1)
self.bottom_banner.color(0)
self.bottom_banner.use_box(True)
self.bottom_banner.box_color(0x808080FF)
self.bottom_banner.set_shadow(0)
self.bottom_banner.set_outline(0)
self.bottom_banner.font(0)
self.close_button = TextDraw.create(
490.666809,
337.829711,
'CLOSE'
)
self.close_button.letter_size(0.128333, 0.957036)
self.close_button.text_size(10.5021, 10.0187)
self.close_button.alignment(2)
self.close_button.color(0xC0C0C0FF)
self.close_button.set_shadow(0)
self.close_button.set_outline(0)
self.close_button.background_color(0x00000033)
self.close_button.font(2)
self.close_button.set_proportional(True)
self.close_button.set_selectable(True)
def show(self, player: Player) -> None:
self.right_arrow.show_for_player(player)
self.left_arrow.show_for_player(player)
self.background.show_for_player(player)
self.top_banner.show_for_player(player)
self.bottom_banner.show_for_player(player)
self.close_button.show_for_player(player)
def hide(self, player: Player) -> None:
self.right_arrow.hide_for_player(player)
self.left_arrow.hide_for_player(player)
self.background.hide_for_player(player)
self.top_banner.hide_for_player(player)
self.bottom_banner.hide_for_player(player)
self.close_button.hide_for_player(player)
@dataclass
class MenuItem:
"""A menu item containing a 3D model and a text label."""
model_id: int
text: str = ''
rot_x: float = 0.
rot_y: float = 0.
rot_z: float = 0.
zoom: float = 1.
@dataclass
class Menu:
"""A menu that has a title and a list of menu items."""
title: str
items: list[MenuItem] = field(default_factory=list)
on_select: Callable[[int, MenuItem], None] = lambda player, item: None
on_cancel: Callable[[int], None] = lambda player: None
def add(self, item: MenuItem) -> None:
if item in self.items:
raise ValueError(
f'Adding the same menu item to the same menu twice: {item:r}'
)
self.items.append(item)
def remove(self, item: MenuItem) -> None:
if item not in self.items:
raise ValueError(
f'Removing menu item not present in menu: {item:r}'
)
self.items.remove(item)
def show(self, player: Player) -> None:
global _player_menus
if not player.is_connected():
return
if _player_menus.get(player.id):
_player_menus[player.id].hide()
_player_menus[player.id] = player_menu = PlayerMenu(self, player)
player_menu.show()
@dataclass
class PlayerMenu:
"""A menu that is being shown to a player. Handles paging, etc."""
menu: Menu
player: Player
_show_time: int = 0
_current_page: int = 0
_total_pages: int = 0
_paging_text: PlayerTextDraw | int = INVALID_TEXT_DRAW
_header_text: PlayerTextDraw | int = INVALID_TEXT_DRAW
_item_models: list[PlayerTextDraw] = field(default_factory=list)
_item_texts: list[PlayerTextDraw] = field(default_factory=list)
def __post_init__(self):
# eg. 18 items in list, 18 items per page, should be one.
# Same for 17, but not for 19.
# This will also be zero if there's no items.
self._total_pages = (len(self.menu.items) - 1) // ITEMS_PER_PAGE + 1
def show(self) -> None:
if not _static_graphics:
raise RuntimeError(
f"{__name__} module isn't initialized. "
f"Did you forget to pyload('{__name__.partition('.')[2]}')?"
)
self._show_time = get_tick_count()
self._destroy_player_textdraws()
self._create_player_textdraws()
for index, item in enumerate(self.menu.items[:ITEMS_PER_PAGE]):
self._show_item_at_index(item, index)
self._paging_text.set_string(f'1/{self._total_pages}')
self._header_text.set_string(self.menu.title)
self._paging_text.show()
self._header_text.show()
_static_graphics.show(self.player)
select_text_draw(self.player.id, 0xFFFFFFFF)
def hide(self) -> None:
global _player_menus
self._destroy_player_textdraws()
_static_graphics.hide(self.player)
cancel_select_text_draw(self.player.id)
del _player_menus[self.player.id]
def set_page(self, page: int) -> None:
if not (0 <= page < self._total_pages):
return
for item_model, item_text in zip(self._item_models, self._item_texts):
item_model.hide()
item_text.hide()
start_index = ITEMS_PER_PAGE * page
for index, item in enumerate(
self.menu.items[start_index:start_index + ITEMS_PER_PAGE]
):
self._show_item_at_index(item, index)
self._current_page = page
self._paging_text.set_string(f'{page + 1}/{self._total_pages}')
def next_page(self) -> None:
if self._current_page >= self._total_pages - 1:
return
self.set_page(self._current_page + 1)
def previous_page(self) -> None:
if self._current_page <= 0:
return
self.set_page(self._current_page - 1)
def showed_at_least(self, ticks: int) -> None:
return get_tick_count() - self._show_time >= ticks
def get_clicked_item(self, textdraw: PlayerTextDraw) -> MenuItem | None:
if textdraw.id not in [td.id for td in self._item_models]:
return None
menu_index = [
index for index, td in enumerate(self._item_models)
if td.id == textdraw.id
][0]
return self.menu.items[
self._current_page * ITEMS_PER_PAGE
+ menu_index
]
def on_select(self, item: MenuItem) -> None:
self.menu.on_select(self.player, item)
def on_cancel(self) -> None:
self.menu.on_cancel(self.player)
def _create_player_textdraws(self) -> None:
self._paging_text = PlayerTextDraw.create(
self.player,
523.333251,
139.792648,
'0/1'
)
self._paging_text.letter_size(0.190666, 1.110518)
self._paging_text.alignment(3)
self._paging_text.color(0xC0C0C0FF)
self._paging_text.set_shadow(0)
self._paging_text.set_outline(1)
self._paging_text.background_color(0x00000033)
self._paging_text.font(2)
self._paging_text.set_proportional(1)
self._header_text = PlayerTextDraw.create(
self.player,
128.333312,
139.377761,
'header'
)
self._header_text.letter_size(0.315000, 1.247407)
self._header_text.alignment(1)
self._header_text.color(0xC0C0C0FF)
self._header_text.set_shadow(0)
self._header_text.set_outline(1)
self._header_text.background_color(0x00000033)
self._header_text.font(2)
self._header_text.set_proportional(True)
item_models, item_texts = self._item_models, self._item_texts
origin = (140., 162.)
for index in range(len(self.menu.items)):
if index >= ITEMS_PER_PAGE:
break
# Hard-coded because that's how it was
current_col = index % 6
current_line = index // 6
current_coords = (
origin[0] + 62 * current_col,
origin[1] + 55 * current_line,
)
item_model = PlayerTextDraw.create(
self.player,
current_coords[0],
current_coords[1],
'_'
)
item_models.append(item_model)
item_model.background_color(0xD3D3D344)
item_model.font(5)
item_model.letter_size(1.430000, 5.700000)
item_model.color(0xC0C0C0FF)
item_model.set_outline(1)
item_model.set_proportional(True)
item_model.use_box(True)
item_model.box_color(0)
item_model.text_size(61.000000, 54.000000)
item_model.set_selectable(True)
item_text = PlayerTextDraw.create(
self.player,
current_coords[0] + 31,
current_coords[1], '_'
)
item_texts.append(item_text)
item_text.font(2)
item_text.letter_size(0.199999, 0.6)
item_text.alignment(2)
item_text.set_outline(0)
item_text.set_proportional(True)
item_text.text_size(0.0, 62.0)
item_text.set_shadow(0)
item_text.color(0xD3D3D3AA)
def _destroy_player_textdraws(self) -> None:
if (
self._paging_text != INVALID_TEXT_DRAW
and self._header_text != INVALID_TEXT_DRAW
):
self._paging_text.destroy()
self._header_text.destroy()
self._paging_text = INVALID_TEXT_DRAW
self._header_text = INVALID_TEXT_DRAW
for item_model, item_text in zip(self._item_models, self._item_texts):
item_model.destroy()
item_text.destroy()
self._item_models = []
self._item_texts = []
def _show_item_at_index(
self, item: MenuItem, index: int
) -> None:
item_model = self._item_models[index]
item_model.set_preview_model(item.model_id)
item_model.set_preview_rotation(
item.rot_x,
item.rot_y,
item.rot_z,
zoom=item.zoom,
)
item_model.show()
if item.text:
item_text = self._item_texts[index]
item_text.set_string(item.text)
item_text.show()
def OnGameModeInit():
global _static_graphics
_static_graphics = StaticGraphics()
@Player.on_click_textdraw
def on_player_click_textdraw(player: Player, clicked: TextDraw) -> None:
global _static_graphics, _player_menus
player_menu = _player_menus.get(player.id)
if not player_menu:
return
# Handle on_select showing another Menu
if not player_menu.showed_at_least(600):
return
if clicked.id in (INVALID_TEXT_DRAW, _static_graphics.close_button.id):
player_menu.on_cancel()
player_menu.hide()
elif clicked.id == _static_graphics.right_arrow.id:
player_menu.next_page()
elif clicked.id == _static_graphics.left_arrow.id:
player_menu.previous_page()
@Player.on_click_playertextdraw
def on_player_click_playertextdraw(
player: Player, textdraw: PlayerTextDraw
) -> None:
global _static_graphics, _player_menus
player_menu = _player_menus.get(player.id)
if not player_menu:
return
# Handle on_select showing another Menu
if not player_menu.showed_at_least(600):
return
item = player_menu.get_clicked_item(textdraw)
if not item:
return
player_menu.hide()
player_menu.on_select(item)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment