253 lines
11 KiB
Python
Executable File
253 lines
11 KiB
Python
Executable File
#!~/.pyenv/versions/3.11.6/bin/python
|
|
#
|
|
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
|
|
#
|
|
# -*- coding:utf-8 -*-
|
|
# @Title: System utilities.
|
|
# @Author: Cutieguwu | Olivia Brooks
|
|
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
|
|
# @Description: Utilities for a PyGame game.
|
|
#
|
|
# @Script: system.py
|
|
# @Date Created: 17 Apr, 2024
|
|
# @Last Modified: 14 Jun, 2024
|
|
# @Last Modified by: Cutieguwu | Olivia Brooks
|
|
# ----------------------------------------------------------
|
|
|
|
"""
|
|
Modules that are loaded will always be loaded globally.
|
|
Some will only be loaded based on conditions, but made global.
|
|
Thus, they can be checked for in globals() and be easily accessible anywhere.
|
|
Even if a module were loaded and unloaded, it would remain in cache, so unloading it is not really worth it.
|
|
Imports will be loaded as the program needs them, thus reducing the initial memory consumption.
|
|
"""
|
|
|
|
# Although they won't be reloaded and the imports will be skipped if called again,
|
|
# avoid wasting processing time by keeping out of import_dependencies()
|
|
from pkg_resources import working_set
|
|
from subprocess import check_call, CalledProcessError
|
|
from sys import executable
|
|
|
|
|
|
def install_dependencies(dependencies:dict):
|
|
"""
|
|
Tries to install and import any missing dependencies from list.
|
|
"""
|
|
|
|
# Adapted from Serge Stroobandt on https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary
|
|
|
|
libsInst = {
|
|
pkg.key for pkg in working_set # Find required and installed libraries.
|
|
}
|
|
|
|
libsMiss = dependencies - libsInst # Remove the already installed libraries from the dependency list.
|
|
|
|
try:
|
|
if len(libsMiss) != 0:
|
|
check_call([executable, "-m", "pip", "install", "--upgrade", "pip"]) # Ensure pip is up-to-date.
|
|
for library in libsMiss:
|
|
check_call([executable, "-m", "pip", "install", library]) # Install all missing libraries.
|
|
|
|
except CalledProcessError:
|
|
print("Error | CANNOT FIND OR INSTALL DEPENDENCY:")
|
|
print(library)
|
|
raise SystemExit
|
|
|
|
install_dependencies({"icecream", "pygame"})
|
|
|
|
from icecream import ic
|
|
from pygame.version import vernum
|
|
from pygame.display import get_driver
|
|
from os import path
|
|
from sys import version as pyver
|
|
from platform import uname
|
|
from json import load as jload
|
|
|
|
|
|
DISPLAY = {}
|
|
PLATFORM = {"libraries": {}}
|
|
PLATFORM["kernel"] = uname().system # `system` is a variable of uname()
|
|
PLATFORM["libraries"]["pygame"] = vernum
|
|
|
|
# Python version cleanup.
|
|
versionStr = ""
|
|
|
|
for c in pyver:
|
|
if c == " ":
|
|
break
|
|
else:
|
|
versionStr = versionStr + c
|
|
|
|
versionTuple = tuple(versionStr.split("."))
|
|
|
|
versionTuple = (int(versionTuple[0]), int(versionTuple[1]), int(versionTuple[2]))
|
|
|
|
PLATFORM["libraries"]["python"] = versionTuple
|
|
|
|
# GCC version cleanup.
|
|
versionStr = ""
|
|
startFlag = False
|
|
|
|
for c in pyver:
|
|
if startFlag:
|
|
if c not in ["G", "C", "]"]:
|
|
versionStr = versionStr + c
|
|
elif c == "[":
|
|
startFlag = True
|
|
|
|
PLATFORM["libraries"]["GCC"] = versionStr
|
|
|
|
del pyver, versionStr, startFlag, versionTuple
|
|
|
|
# Fetch compatability settings.
|
|
DIRWORKING = path.dirname(__file__) + "/.."
|
|
|
|
with open(DIRWORKING + "/config/features.json") as f:
|
|
FEATURES = jload(f)
|
|
|
|
# All features in file default to is_active: false
|
|
# Enable supported features.
|
|
|
|
for feature in FEATURES.items():
|
|
is_active = False
|
|
|
|
for lib in feature[1]["libs"].items():
|
|
if PLATFORM["libraries"][lib[0]] > tuple(lib[1]["ver_implemented"]):
|
|
is_active = True
|
|
|
|
FEATURES[feature[0]]["is_active"] = is_active
|
|
|
|
class Compat():
|
|
def linux():
|
|
# Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
|
|
|
|
"""
|
|
if "environ" not in globals(): # Import if not already.
|
|
global environ
|
|
from os import environ
|
|
|
|
try:
|
|
if environ["WAYLAND_DISPLAY"] != "" and globals["XDG_SESSION_TYPE"] != "x11": # Check for environment variable present on Wayland systems.
|
|
print("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")
|
|
|
|
except KeyError: # Wayland keys won't exist on X11 systems (mostly).
|
|
install_dependencies({"Xlib"})
|
|
global display
|
|
from Xlib import display
|
|
global randr
|
|
from Xlib.ext import randr
|
|
|
|
displayGlobal = display.Display() # Get every display thing. Maybe a WM object?
|
|
default_screen = displayGlobal.get_default_screen() # Find default display for some reason.
|
|
info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens.
|
|
|
|
displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations.
|
|
configsActive = set() # Create an empty set to add configurations to.
|
|
|
|
for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use.
|
|
crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
|
|
if crtc_info.mode:
|
|
configsActive.add(crtc_info.mode)
|
|
|
|
for config in displayConfigs.modes:
|
|
if config.id in configsActive: # For every active display mode, figure out its framerate.
|
|
framerate = config.dot_clock / (config.h_total * config.v_total)
|
|
"""
|
|
|
|
if get_driver() == "wayland":
|
|
ic("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")
|
|
elif get_driver() == "x11":
|
|
install_dependencies({"Xlib"})
|
|
global display
|
|
from Xlib import display
|
|
global randr
|
|
from Xlib.ext import randr
|
|
|
|
displayGlobal = display.Display() # Get every display thing. Maybe a WM object?
|
|
default_screen = displayGlobal.get_default_screen() # Find default display for some reason.
|
|
info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens.
|
|
|
|
displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations.
|
|
configsActive = set() # Create an empty set to add configurations to.
|
|
|
|
for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use.
|
|
crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
|
|
if crtc_info.mode:
|
|
configsActive.add(crtc_info.mode)
|
|
|
|
framerate = 30
|
|
|
|
# With multiple displays, get the highest framerate that any one monitor can do.
|
|
for config in displayConfigs.modes:
|
|
if config.id in configsActive: # For every active display mode, figure out its framerate.
|
|
framerateDisplay = (config.dot_clock / (config.h_total * config.v_total))
|
|
if framerateDisplay > framerate:
|
|
framerate = framerateDisplay
|
|
|
|
return framerate
|
|
|
|
def nt():
|
|
# Cannot use pygame.display.get_driver for windows or macos in pygame < v2
|
|
|
|
# Adapted from Anurag Uniyal on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
|
|
|
|
install_dependencies({"pywin32"})
|
|
|
|
if "EnumDisplayDevices" not in globals(): # Import if not already.
|
|
global EnumDisplayDevices
|
|
from win32api import EnumDisplayDevices
|
|
if "EnumDisplaySettings" not in globals(): # Import if not already.
|
|
global EnumDisplaySettings
|
|
from win32api import EnumDisplaySettings
|
|
|
|
display_info = EnumDisplayDevices() # Returns PyDISPLAY_DEVICE object
|
|
|
|
display_settings = EnumDisplaySettings(display_info.DeviceName, -1) # Picks display that seems to have highest framerate?...
|
|
return getattr(display_settings, "DisplayFrequency")
|
|
|
|
def darwin():
|
|
# Cannot use pygame.display.get_driver for windows or macos in pygame < v2
|
|
|
|
# Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
|
|
|
|
install_dependencies({"Appkit"})
|
|
|
|
if "NSScreen" not in globals(): # Import if not already.
|
|
global NSScreen
|
|
from AppKit import NSScreen
|
|
|
|
for screen in NSScreen.screens():
|
|
if framerate < screen.maximumFramesPerSecond(): # I the framerate of this display is higher, set its as the new maximum rate.
|
|
framerate = screen.maximumFramesPerSecond()
|
|
|
|
return framerate
|
|
|
|
def update_framerate():
|
|
"""
|
|
Used for mock vsync to support Pygame < v2.0.0
|
|
"""
|
|
|
|
if FEATURES["matchcase"]["is_active"]:
|
|
match PLATFORM["kernel"]:
|
|
case "Linux":
|
|
framerate = Compat.linux()
|
|
case "Windows":
|
|
framerate = Compat.nt()
|
|
case "Darwin":
|
|
framerate = Compat.darwin()
|
|
|
|
elif PLATFORM["kernel"] == "Linux":
|
|
framerate = Compat.linux()
|
|
|
|
elif PLATFORM["kernel"] == "Windows":
|
|
framerate = Compat.nt()
|
|
|
|
elif PLATFORM["kernel"] == "Darwin": # I'm trusting that the info I found for MacOS does actually still work this way.
|
|
framerate = Compat.darwin()
|
|
|
|
else:
|
|
print("Unsupported or unable to detect system kernel. Defaulting to 30 FPS. This can be overridden in the user settings file.")
|
|
|
|
framerate = int("%.0f" % framerate)
|
|
DISPLAY["framerate"] = framerate
|