├── assets ├── app_preview.png ├── Traces_OFF.svg ├── Traces_ON.svg └── bg.svg ├── apps └── python │ └── traces │ ├── img │ └── bg.png │ ├── dll │ ├── stdlib │ │ └── _ctypes.pyd │ └── stdlib64 │ │ └── _ctypes.pyd │ ├── color_palette.py │ ├── config_defaults.ini │ ├── app_window.py │ ├── ac_data.py │ ├── ac_label.py │ ├── config_handler.py │ ├── traces.py │ ├── lib │ └── sim_info.py │ ├── drawables.py │ └── ac_gl_utils.py ├── content ├── fonts │ ├── ACRoboto300.ttf │ └── ACRoboto700.ttf └── gui │ └── icons │ ├── Traces_OFF.png │ └── Traces_ON.png ├── LICENSE ├── readme.md └── .gitignore /assets/app_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/assets/app_preview.png -------------------------------------------------------------------------------- /apps/python/traces/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/apps/python/traces/img/bg.png -------------------------------------------------------------------------------- /content/fonts/ACRoboto300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/content/fonts/ACRoboto300.ttf -------------------------------------------------------------------------------- /content/fonts/ACRoboto700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/content/fonts/ACRoboto700.ttf -------------------------------------------------------------------------------- /content/gui/icons/Traces_OFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/content/gui/icons/Traces_OFF.png -------------------------------------------------------------------------------- /content/gui/icons/Traces_ON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/content/gui/icons/Traces_ON.png -------------------------------------------------------------------------------- /apps/python/traces/dll/stdlib/_ctypes.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/apps/python/traces/dll/stdlib/_ctypes.pyd -------------------------------------------------------------------------------- /apps/python/traces/dll/stdlib64/_ctypes.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frits-z/ac-traces/HEAD/apps/python/traces/dll/stdlib64/_ctypes.pyd -------------------------------------------------------------------------------- /apps/python/traces/color_palette.py: -------------------------------------------------------------------------------- 1 | 2 | class Colors: 3 | """Class-level attributes with RGBA color tuples""" 4 | green = (0.16, 1, 0, 1) 5 | red = (1, 0.16, 0 , 1) 6 | blue = (0.16, 1, 1, 1) 7 | grey = (0.35, 0.35, 0.35, 1) 8 | light_grey = (0.6, 0.6, 0.6, 1) 9 | yellow = (1, 0.8, 0, 1) -------------------------------------------------------------------------------- /apps/python/traces/config_defaults.ini: -------------------------------------------------------------------------------- 1 | [GENERAL] 2 | app_height=125 ; App height (Specifies the height of the app in pixels); from 50 to 500 3 | use_kmh=True ; Use km/h; "True" or "False" 4 | 5 | [TRACES] 6 | display_throttle=True ; Display throttle pedal trace; "True" or "False" 7 | display_brake=True ; Display brake pedal trace; "True" or "False" 8 | display_clutch=False ; Display clutch pedal trace; "True" or "False" 9 | display_steering=True ; Display steering wheel trace; "True" or "False" 10 | trace_time_window=7 ; Trace time window; from 4 seconds to 10 seconds 11 | trace_sample_rate=15 ; Traces sample rate; from 10 hz to 30 hz 12 | trace_thickness=3.0 ; Trace line thickness; from 1 px to 10 px 13 | trace_steering_cap=180.0 ; Max steering angle for trace; from 90 degrees to 360 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Frits van der Zalm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Traces 2 | 3 | Traces is a Python app for Assetto Corsa. It plots the driver's pedal and steering input telemetry on a graph in real time. Additionally, it includes pedal input bars, a force-feedback meter, steering wheel indicator, speedometer and gear indicator. 4 | 5 | ![App Preview](/assets/app_preview.png) 6 | 7 | ## Installation 8 | 9 | Install the app by pasting the contents of the .zip file in the main root folder of the Assetto Corsa installation. After this, the app must be activated in the main menu within Assetto Corsa. 10 | 11 | ## Configuration 12 | 13 | The app is user configurable and is integrated with Content Manager. After first launch, options like app size can be tweaked using the config.ini file in the app folder. 14 | 15 | ## Notes 16 | 17 | * Due to the absence of full OpenGL implementation by Kunos for Python apps, the drawing of the telemetry lines is quite resource intensive. This scales directly with the number of input traces (drawing two traces is heavier than one), the sample rate and the time length of the graph. 18 | * Currently when the app is not visible, it still carries out some calculations in the background. This costs some performance and may get fixed in the future. 19 | 20 | ## Credits 21 | 22 | * Rombik, for the Assetto Corsa shared memory library. -------------------------------------------------------------------------------- /apps/python/traces/app_window.py: -------------------------------------------------------------------------------- 1 | import ac 2 | 3 | class AppWindow: 4 | """Main window of the app. 5 | 6 | Args: 7 | cfg (obj:Config): Config object used to set 8 | attributes for the app window. 9 | """ 10 | def __init__(self, cfg): 11 | # Config data 12 | self.cfg = cfg 13 | 14 | # Set up app window 15 | self.id = ac.newApp(self.cfg.app_name) 16 | ac.setSize(self.id, self.cfg.app_width, self.cfg.app_height) 17 | self.bg_texture_path = "apps/python/traces/img/bg.png" 18 | ac.setBackgroundTexture(self.id, self.bg_texture_path) 19 | ac.setBackgroundOpacity(self.id, 0) 20 | ac.drawBorder(self.id, 0) 21 | ac.setTitle(self.id, "") 22 | ac.setIconPosition(self.id, 0, -10000) 23 | 24 | # Initialize empty list of drawable objects. 25 | self.drawables = [] 26 | 27 | def add_drawable(self, obj): 28 | """Add drawable object to list of drawables""" 29 | if obj not in self.drawables: 30 | self.drawables.append(obj) 31 | 32 | def remove_drawable(self, obj): 33 | """Remove drawable object from list of drawables""" 34 | if obj in self.drawables: 35 | self.drawables.remove(obj) 36 | 37 | def render(self, deltaT): 38 | """Draw graphics elements on the app window. 39 | 40 | Args: 41 | deltaT (float): Time delta since last tick in seconds. 42 | Assetto Corsa passes this argument automatically. 43 | 44 | This method calls the draw method on each object in the list of drawables. 45 | This method should be called on render callback of Assetto Corsa. 46 | """ 47 | # When the user moves the window, the opacity is reset to default. 48 | # Therefore, opacity needs to be set to 0 every frame. 49 | ac.setBackgroundOpacity(self.id, 0) 50 | 51 | for drawable in self.drawables: 52 | drawable.draw() 53 | 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Project specific gitignore 2 | 3 | # VS Code 4 | .vscode/ 5 | 6 | # GIMP Project files 7 | gimpprojects/ 8 | 9 | # Test notebooks 10 | *.ipynb 11 | 12 | ## Standard Python gitignore template 13 | # https://github.com/github/gitignore/blob/master/Python.gitignore 14 | 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | cover/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | # pytype static type analyzer 148 | .pytype/ 149 | 150 | # Cython debug symbols 151 | cython_debug/ -------------------------------------------------------------------------------- /apps/python/traces/ac_data.py: -------------------------------------------------------------------------------- 1 | import ac 2 | import acsys 3 | 4 | import os 5 | import sys 6 | import platform 7 | import math 8 | 9 | # Import Assetto Corsa shared memory library. 10 | # It has a dependency on ctypes, which is not included in AC python version. 11 | # Point to correct ctypes module based on platform architecture. 12 | # First, get directory of the app, then add correct folder to sys.path. 13 | app_dir = os.path.dirname(__file__) 14 | 15 | if platform.architecture()[0] == "64bit": 16 | sysdir = os.path.join(app_dir, 'dll', 'stdlib64') 17 | else: 18 | sysdir = os.path.join(app_dir, 'dll', 'stdlib') 19 | # Python looks in sys.path for modules to load, insert new dir first in line. 20 | sys.path.insert(0, sysdir) 21 | os.environ['PATH'] = os.environ['PATH'] + ";." 22 | 23 | from lib.sim_info import info 24 | 25 | 26 | class ACGlobalData: 27 | """Handling all data from AC that is not car-specific. 28 | 29 | Args: 30 | cfg (obj:Config): App configuration. 31 | """ 32 | def __init__(self, cfg): 33 | # Config object 34 | self.cfg = cfg 35 | 36 | # Data attributes 37 | self.focused_car = 0 38 | self.replay_time_multiplier = 1 39 | 40 | def update(self): 41 | """Update data.""" 42 | self.focused_car = ac.getFocusedCar() 43 | self.replay_time_multiplier = info.graphics.replayTimeMultiplier 44 | 45 | 46 | class ACCarData: 47 | """Handling all data from AC that is car-specific. 48 | 49 | Args: 50 | cfg (obj:Config): App configuration. 51 | car_id (int, optional): Car ID number to retrieve data from. 52 | Defaults to own car. 53 | """ 54 | def __init__(self, cfg, car_id=0): 55 | self.cfg = cfg 56 | self.car_id = car_id 57 | 58 | # Initialize data attributes 59 | self.speed = 0 60 | self.throttle = 0 61 | self.brake = 0 62 | self.clutch = 0 63 | self.gear = 0 64 | self.steering = 0 65 | self.ffb = 0 66 | 67 | # Normalized steering for steering trace 68 | self.steering_normalized = 0.5 69 | self.steering_cap = self.cfg.trace_steering_cap * math.pi / 180 70 | 71 | self.gear_text = "N" 72 | 73 | def set_car_id(self, car_id): 74 | """Update car ID to retrieve data from. 75 | 76 | Args: 77 | car_id (int): Car ID number.""" 78 | self.car_id = car_id 79 | 80 | def update(self): 81 | """Update data.""" 82 | self.throttle = ac.getCarState(self.car_id, acsys.CS.Gas) 83 | self.brake = ac.getCarState(self.car_id, acsys.CS.Brake) 84 | self.clutch = 1 - ac.getCarState(self.car_id, acsys.CS.Clutch) 85 | self.ffb = ac.getCarState(self.car_id, acsys.CS.LastFF) 86 | self.steering = ac.getCarState(self.car_id, acsys.CS.Steer) * math.pi / 180 87 | self.gear = ac.getCarState(self.car_id, acsys.CS.Gear) 88 | 89 | if self.cfg.use_kmh: 90 | self.speed = ac.getCarState(self.car_id, acsys.CS.SpeedKMH) 91 | else: 92 | self.speed = ac.getCarState(self.car_id, acsys.CS.SpeedMPH) 93 | 94 | self.steering_normalized = 0.5 - (self.steering / (2 * self.steering_cap)) 95 | if self.steering_normalized > 1: 96 | self.steering_normalized = 1 97 | elif self.steering_normalized < 0: 98 | self.steering_normalized = 0 99 | 100 | # Gear label 101 | if self.gear == 0: 102 | self.gear_text = "R" 103 | elif self.gear == 1: 104 | self.gear_text = "N" 105 | else: 106 | self.gear_text = str(self.gear - 1) -------------------------------------------------------------------------------- /assets/Traces_OFF.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 64 | 68 | 73 | 78 | 79 | 83 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /assets/Traces_ON.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 57 | 63 | 64 | 68 | 73 | 78 | 79 | 83 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /apps/python/traces/ac_label.py: -------------------------------------------------------------------------------- 1 | import ac 2 | from ac_gl_utils import Point 3 | 4 | class ACLabel: 5 | """Initialize Assetto Corsa text label. 6 | 7 | Args: 8 | window_id (obj:Renderer.id): 9 | position (obj:Point): Set x, y positon of label by passing a Point object. 10 | Optional. Defaults to 0, 0. 11 | text (str): 12 | font (str): Custom font name 13 | italics (0, 1): 1 for italics, 0 for regular. 14 | color (tuple): r,g,b,a on a 0-1 scale. 15 | size (int): Font size. 16 | alignment (str): "left", "center", "right" 17 | prefix (str): Prefix before main text. 18 | postfix (str): Postfix after main text. 19 | """ 20 | def __init__(self, window_id, position=Point(), text=" ", font=None, italic=0, size=None, color=None, alignment='left', prefix="", postfix=""): 21 | # Create label 22 | self.id = ac.addLabel(window_id, "") 23 | # Set position 24 | self.set_position(position) 25 | # Set text 26 | self.prefix = prefix 27 | self.postfix = postfix 28 | self.set_text(text) 29 | # Set alignment 30 | self.set_alignment(alignment) 31 | # Optional items 32 | if font is not None: 33 | self.set_custom_font(font, italic) 34 | if size is not None: 35 | self.set_font_size(size) 36 | if color is not None: 37 | self.set_color(color) 38 | 39 | def fill_height(self, position, height): 40 | """Set text label to position and fill height, may overflow vertically. 41 | 42 | Args: 43 | position (obj:Point): Point object with x,y coords. 44 | height (float): Height in pixels that the text label should fill. 45 | 46 | Important! Designed for Roboto font family. 47 | """ 48 | # Calculate font size based on box height 49 | font_size = 1.4 * height 50 | # Adjust y pos. 51 | position.y -= (1/3) * height 52 | self.set_font_size(font_size) 53 | self.set_position(position) 54 | 55 | def fit_height(self, position, height): 56 | """Set text label to position and fit label centered in given height with adequate spacing. 57 | 58 | Args: 59 | position (obj:Point): Point object with x,y coords. 60 | height (float): Height in pixels that the text label should be centered fit within. 61 | 62 | Important! Designed for Roboto font family. 63 | """ 64 | # Calculate font size based on box height 65 | font_size = 0.84 * height 66 | # No need to make adjustment to position. 67 | self.set_font_size(font_size) 68 | self.set_position(position) 69 | 70 | def set_position(self, position): 71 | """Set label position. 72 | 73 | Args: 74 | position (obj:Point): Point object with x,y coords. 75 | """ 76 | ac.setPosition(self.id, position.x, position.y) 77 | 78 | def set_prefix(self, prefix): 79 | """Set label prefix. 80 | 81 | Args: 82 | prefix (str): Label prefix. 83 | """ 84 | self.prefix = prefix 85 | 86 | def set_postfix(self, postfix): 87 | """Set label postfix. 88 | 89 | Args: 90 | postfix (str): Label postfix. 91 | """ 92 | self.postfix = postfix 93 | 94 | def set_text(self, text): 95 | """Set label text, making use of set pre/postfixes. 96 | 97 | Args: 98 | text (str): Label text. 99 | """ 100 | text = self.prefix + text + self.postfix 101 | ac.setText(self.id, text) 102 | 103 | def set_alignment(self, alignment='left'): 104 | """Set text horizontal alignment 105 | 106 | Args: 107 | alignment (str): 'left', 'center', 'right'. 108 | defaults to left. 109 | """ 110 | ac.setFontAlignment(self.id, alignment) 111 | 112 | def set_font_size(self, size): 113 | """Set text label font size. 114 | 115 | Args: 116 | size (float): Fontsize in PIXELS (not pt) 117 | 118 | Important: Fontsize in Assetto Corsa is done in pixels, not pt. 119 | Therefore vertically it scales linearly. 120 | """ 121 | ac.setFontSize(self.id, size) 122 | 123 | def set_custom_font(self, font, italic=0): 124 | """Set custom font for text label. 125 | 126 | Args: 127 | font (str): Name of the font, must be initialized. 128 | italic (0/1): Optional, italics yes/no. 129 | """ 130 | ac.setCustomFont(self.id, font, italic, 0) 131 | 132 | def set_color(self, color): 133 | """Set Color for Label. 134 | 135 | Args: 136 | color (tuple): r,g,b,a on a 0-1 scale. 137 | """ 138 | ac.setFontColor(self.id, color[0], color[1], color[2], color[3]) -------------------------------------------------------------------------------- /apps/python/traces/config_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | 4 | class Config: 5 | """App configuration. Load config upon intialization.""" 6 | def __init__(self): 7 | # Set up config paths 8 | self.app_dir = os.path.dirname(__file__) 9 | self.cfg_file_path = os.path.join(self.app_dir, "config.ini") 10 | self.defaults_file_path = os.path.join(self.app_dir, "config_defaults.ini") 11 | 12 | # Set app attributes that are non-configurable by user, 13 | # which therefore don't appear in the config file. 14 | self.app_name = "Traces" 15 | self.app_aspect_ratio = 4.27 16 | self.app_padding = 0.1 # Fraction of app height 17 | 18 | # Load config 19 | self.update_cfg = False 20 | self.load() 21 | 22 | def load(self): 23 | """Initialize config parser and load config""" 24 | # Load config file parser 25 | self.cfg_parser = configparser.ConfigParser() 26 | self.cfg_parser.read(self.cfg_file_path) 27 | # Load config defaults file parser 28 | self.defaults_parser = configparser.ConfigParser(inline_comment_prefixes=";") 29 | self.defaults_parser.read(self.defaults_file_path) 30 | 31 | # Loop over sections in defaults. If any are missing in cfg, add them. 32 | for section in self.defaults_parser.sections(): 33 | if not self.cfg_parser.has_section(section): 34 | self.cfg_parser.add_section(section) 35 | 36 | # Load attributes from config. 37 | # If option is missing, get option from defaults and replace. 38 | self.getint('GENERAL', 'app_height') 39 | self.getbool('GENERAL', 'use_kmh') 40 | 41 | self.getbool('TRACES', 'display_throttle') 42 | self.getbool('TRACES', 'display_brake') 43 | self.getbool('TRACES', 'display_clutch') 44 | self.getbool('TRACES', 'display_steering') 45 | self.getint('TRACES', 'trace_time_window') 46 | self.getint('TRACES', 'trace_sample_rate') 47 | self.getfloat('TRACES', 'trace_thickness') 48 | self.getfloat('TRACES', 'trace_steering_cap') 49 | 50 | # Generate attributes derived from config options 51 | self.app_width = self.app_height * self.app_aspect_ratio 52 | self.app_scale = self.app_height / 500 53 | 54 | # If update_cfg has been triggered (set to True), run save to update file. 55 | if self.update_cfg: 56 | self.save() 57 | 58 | 59 | def save(self): 60 | """Save config file""" 61 | with open(self.cfg_file_path, 'w') as cfgfile: 62 | self.cfg_parser.write(cfgfile) 63 | 64 | 65 | def getfloat(self, section, option): 66 | """Get float variable from config. 67 | 68 | If missing from config, grab from defaults and save it to config. 69 | 70 | Args: 71 | section (str): Section in config file 72 | options (str): Option with specified section 73 | """ 74 | try: 75 | value = self.cfg_parser.getfloat(section, option) 76 | except: 77 | value = self.defaults_parser.getfloat(section, option) 78 | self.cfg_parser.set(section, option, str(value)) 79 | self.update_cfg = True 80 | self.__setattr__(option, value) 81 | 82 | 83 | def getbool(self, section, option): 84 | """Get boolean variable from config. 85 | 86 | If missing from config, grab from defaults and save it to config. 87 | 88 | Args: 89 | section (str): Section in config file 90 | options (str): Option with specified section 91 | """ 92 | try: 93 | value = self.cfg_parser.getboolean(section, option) 94 | except: 95 | value = self.defaults_parser.getboolean(section, option) 96 | self.cfg_parser.set(section, option, str(value)) 97 | self.update_cfg = True 98 | self.__setattr__(option, value) 99 | 100 | 101 | def getint(self, section, option): 102 | """Get integer variable from config. 103 | 104 | If missing from config, grab from defaults and save it to config. 105 | 106 | Args: 107 | section (str): Section in config file 108 | options (str): Option with specified section 109 | """ 110 | try: 111 | value = self.cfg_parser.getint(section, option) 112 | except: 113 | try: 114 | value = int(self.cfg_parser.getfloat(section, option)) 115 | except: 116 | value = self.defaults_parser.getint(section, option) 117 | self.cfg_parser.set(section, option, str(value)) 118 | self.update_cfg = True 119 | self.__setattr__(option, value) 120 | 121 | 122 | def getstr(self, section, option): 123 | """Get string variable from config. 124 | 125 | If missing from config, grab from defaults and save it to config. 126 | 127 | Args: 128 | section (str): Section in config file 129 | options (str): Option with specified section 130 | """ 131 | try: 132 | value = self.cfg_parser.get(section, option) 133 | except: 134 | value = self.defaults_parser.get(section, option) 135 | self.cfg_parser.set(section, option, str(value)) 136 | self.update_cfg = True 137 | self.__setattr__(option, value) 138 | -------------------------------------------------------------------------------- /apps/python/traces/traces.py: -------------------------------------------------------------------------------- 1 | import ac 2 | 3 | from color_palette import Colors 4 | from config_handler import Config 5 | from ac_data import ACGlobalData, ACCarData 6 | from drawables import Trace, PedalBar, SteeringWheel 7 | from app_window import AppWindow 8 | from ac_label import ACLabel 9 | from ac_gl_utils import Point 10 | 11 | # Initialize general object variables 12 | cfg = None 13 | ac_global_data = None 14 | ac_car_data = None 15 | app_window = None 16 | 17 | # Trace drawable objects 18 | throttle_trace = None 19 | brake_trace = None 20 | clutch_trace = None 21 | steering_trace = None 22 | 23 | # Timers 24 | timer_60_hz = 0 25 | timer_10_hz = 0 26 | timer_trace = 0 27 | trace_update_batch = 0 28 | 29 | PERIOD_60_HZ = 1 / 60 30 | PERIOD_10_HZ = 1 / 10 31 | 32 | # Pedal bars drawable objects 33 | throttle_bar = None 34 | brake_bar = None 35 | clutch_bar = None 36 | ffb_bar = None 37 | 38 | # Wheel indicator drawable object 39 | wheel_indicator = None 40 | 41 | # Text labels 42 | label_speed = 0 43 | label_gear = 0 44 | 45 | 46 | def acMain(ac_version): 47 | """Run upon startup of Assetto Corsa. 48 | 49 | Args: 50 | ac_version (str): Version of Assetto Corsa. 51 | AC passes this argument automatically. 52 | """ 53 | # Read config 54 | global cfg 55 | cfg = Config() 56 | 57 | # Initialize ac data objects 58 | global ac_global_data, ac_car_data 59 | ac_global_data = ACGlobalData(cfg) 60 | ac_car_data = ACCarData(cfg) 61 | 62 | # Set up app window 63 | global app_window 64 | app_window = AppWindow(cfg) 65 | ac.addRenderCallback(app_window.id, app_render) 66 | 67 | # Initialize trace objects and add to drawables list 68 | global throttle_trace, brake_trace, clutch_trace, steering_trace 69 | if cfg.display_steering: 70 | steering_trace = Trace(cfg, ac_global_data, Colors.light_grey) 71 | app_window.add_drawable(steering_trace) 72 | if cfg.display_clutch: 73 | clutch_trace = Trace(cfg, ac_global_data, Colors.blue) 74 | app_window.add_drawable(clutch_trace) 75 | if cfg.display_throttle: 76 | throttle_trace = Trace(cfg, ac_global_data, Colors.green) 77 | app_window.add_drawable(throttle_trace) 78 | if cfg.display_brake: 79 | brake_trace = Trace(cfg, ac_global_data, Colors.red) 80 | app_window.add_drawable(brake_trace) 81 | 82 | # Initialize pedal bars objects and add to drawables list 83 | global throttle_bar, brake_bar, clutch_bar, ffb_bar 84 | throttle_bar = PedalBar(cfg, 1555, Colors.green) 85 | app_window.add_drawable(throttle_bar) 86 | brake_bar = PedalBar(cfg, 1480, Colors.red) 87 | app_window.add_drawable(brake_bar) 88 | clutch_bar = PedalBar(cfg, 1405, Colors.blue) 89 | app_window.add_drawable(clutch_bar) 90 | ffb_bar = PedalBar(cfg, 1630, Colors.grey) 91 | app_window.add_drawable(ffb_bar) 92 | 93 | # Initialize wheel indicator and add to drawables list 94 | global wheel_indicator 95 | wheel_indicator = SteeringWheel(cfg, Colors.yellow) 96 | app_window.add_drawable(wheel_indicator) 97 | 98 | # Initialize fonts 99 | ac.initFont(0, 'ACRoboto300', 0, 0) 100 | ac.initFont(0, 'ACRoboto700', 0, 0) 101 | 102 | # Set up labels 103 | global label_speed, label_gear 104 | label_speed = ACLabel(app_window.id, font='ACRoboto300', alignment='center') 105 | label_speed.fill_height(Point(1935 * cfg.app_scale, cfg.app_padding * cfg.app_height), 50 * cfg.app_scale) 106 | 107 | # Speed unit selection 108 | if cfg.use_kmh: 109 | label_speed.set_postfix(" km/h") 110 | else: 111 | label_speed.set_postfix(" mph") 112 | 113 | label_gear = ACLabel(app_window.id, font='ACRoboto700', alignment='center') 114 | label_gear.fit_height(Point(1935 * cfg.app_scale, (300 - 112) * cfg.app_scale), 224 * cfg.app_scale) 115 | 116 | 117 | def acUpdate(deltaT): 118 | """Run every physics tick of Assetto Corsa. 119 | 120 | Args: 121 | deltaT (float): Time delta since last tick in seconds. 122 | Assetto Corsa passes this argument automatically. 123 | """ 124 | global timer_60_hz, timer_10_hz 125 | global timer_trace 126 | global trace_update_batch 127 | 128 | # Update timers 129 | timer_60_hz += deltaT 130 | timer_10_hz += deltaT 131 | timer_trace += deltaT 132 | 133 | # Run on 10hz 134 | if timer_10_hz > PERIOD_10_HZ: 135 | timer_10_hz -= PERIOD_10_HZ 136 | 137 | # Update ac global data 138 | ac_global_data.update() 139 | 140 | # Set car id for car data 141 | ac_car_data.set_car_id(ac_global_data.focused_car) 142 | 143 | # Update text labels 144 | label_speed.set_text("{:.0f}".format(ac_car_data.speed)) 145 | label_gear.set_text("{}".format(ac_car_data.gear_text)) 146 | 147 | # Run on 60hz 148 | if timer_60_hz > PERIOD_60_HZ: 149 | timer_60_hz -= PERIOD_60_HZ 150 | 151 | # Update ac car data 152 | ac_car_data.update() 153 | 154 | # Update data for pedalbar and wheelindicator drawables 155 | wheel_indicator.update(ac_car_data.steering) 156 | throttle_bar.update(ac_car_data.throttle) 157 | brake_bar.update(ac_car_data.brake) 158 | clutch_bar.update(ac_car_data.clutch) 159 | 160 | # Set FFB bar to red if FFB is clipping (greater than 1) 161 | if ac_car_data.ffb < 1: 162 | ffb_bar.color = Colors.grey 163 | ffb_bar.update(ac_car_data.ffb) 164 | else: 165 | ffb_bar.color = Colors.red 166 | ffb_bar.update(1) 167 | 168 | # Update traces data in batches 169 | # This is done to spread out calc load over physics update ticks. 170 | if timer_trace > (1 / cfg.trace_sample_rate): 171 | trace_update_batch += 1 172 | 173 | if trace_update_batch == 1: 174 | if cfg.display_clutch: 175 | clutch_trace.update(ac_car_data.clutch) 176 | 177 | elif trace_update_batch == 2: 178 | if cfg.display_steering: 179 | steering_trace.update(ac_car_data.steering_normalized) 180 | 181 | elif trace_update_batch == 3: 182 | if cfg.display_throttle: 183 | throttle_trace.update(ac_car_data.throttle) 184 | 185 | else: 186 | if cfg.display_brake: 187 | brake_trace.update(ac_car_data.brake) 188 | 189 | # On final batch, reset counter and timer 190 | trace_update_batch = 0 191 | timer_trace -= (1 / cfg.trace_sample_rate) 192 | 193 | 194 | def app_render(deltaT): 195 | """Run every rendered frame of Assetto Corsa. 196 | 197 | Args: 198 | deltaT (float): Time delta since last tick in seconds. 199 | Assetto Corsa passes this argument automatically. 200 | """ 201 | app_window.render(deltaT) 202 | 203 | 204 | def acShutdown(): 205 | """Run on shutdown of Assetto Corsa""" 206 | # Update config if necessary 207 | if cfg.update_cfg: 208 | cfg.save() -------------------------------------------------------------------------------- /assets/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 67 | 68 | 73 | 76 | 81 | 89 | 96 | 103 | 110 | 117 | 125 | 128 | 135 | 142 | 149 | 156 | 157 | 158 | 159 | 176 | 177 | -------------------------------------------------------------------------------- /apps/python/traces/lib/sim_info.py: -------------------------------------------------------------------------------- 1 | import mmap 2 | import functools 3 | import ctypes 4 | from ctypes import c_int32, c_float, c_wchar 5 | 6 | AC_STATUS = c_int32 7 | AC_OFF = 0 8 | AC_REPLAY = 1 9 | AC_LIVE = 2 10 | AC_PAUSE = 3 11 | AC_SESSION_TYPE = c_int32 12 | AC_UNKNOWN = -1 13 | AC_PRACTICE = 0 14 | AC_QUALIFY = 1 15 | AC_RACE = 2 16 | AC_HOTLAP = 3 17 | AC_TIME_ATTACK = 4 18 | AC_DRIFT = 5 19 | AC_DRAG = 6 20 | AC_FLAG_TYPE = c_int32 21 | AC_NO_FLAG = 0 22 | AC_BLUE_FLAG = 1 23 | AC_YELLOW_FLAG = 2 24 | AC_BLACK_FLAG = 3 25 | AC_WHITE_FLAG = 4 26 | AC_CHECKERED_FLAG = 5 27 | AC_PENALTY_FLAG = 6 28 | 29 | class SPageFilePhysics(ctypes.Structure): 30 | _pack_ = 4 31 | _fields_ = [ 32 | ('packetId', c_int32), 33 | ('gas', c_float), 34 | ('brake', c_float), 35 | ('fuel', c_float), 36 | ('gear', c_int32), 37 | ('rpms', c_int32), 38 | ('steerAngle', c_float), 39 | ('speedKmh', c_float), 40 | ('velocity', c_float * 3), 41 | ('accG', c_float * 3), 42 | ('wheelSlip', c_float * 4), 43 | ('wheelLoad', c_float * 4), 44 | ('wheelsPressure', c_float * 4), 45 | ('wheelAngularSpeed', c_float * 4), 46 | ('tyreWear', c_float * 4), 47 | ('tyreDirtyLevel', c_float * 4), 48 | ('tyreCoreTemperature', c_float * 4), 49 | ('camberRAD', c_float * 4), 50 | ('suspensionTravel', c_float * 4), 51 | ('drs', c_float), 52 | ('tc', c_float), 53 | ('heading', c_float), 54 | ('pitch', c_float), 55 | ('roll', c_float), 56 | ('cgHeight', c_float), 57 | ('carDamage', c_float * 5), 58 | ('numberOfTyresOut', c_int32), 59 | ('pitLimiterOn', c_int32), 60 | ('abs', c_float), 61 | ('kersCharge', c_float), 62 | ('kersInput', c_float), 63 | ('autoShifterOn', c_int32), 64 | ('rideHeight', c_float * 2), 65 | ('turboBoost', c_float), 66 | ('ballast', c_float), 67 | ('airDensity', c_float), 68 | ('airTemp', c_float), 69 | ('roadTemp', c_float), 70 | ('localAngularVel', c_float * 3), 71 | ('finalFF', c_float), 72 | ('performanceMeter', c_float), 73 | ('engineBrake', c_int32), 74 | ('ersRecoveryLevel', c_int32), 75 | ('ersPowerLevel', c_int32), 76 | ('ersHeatCharging', c_int32), 77 | ('ersIsCharging', c_int32), 78 | ('kersCurrentKJ', c_float), 79 | ('drsAvailable', c_int32), 80 | ('drsEnabled', c_int32), 81 | ('brakeTemp', c_float * 4), 82 | ('clutch', c_float), 83 | ('tyreTempI', c_float * 4), 84 | ('tyreTempM', c_float * 4), 85 | ('tyreTempO', c_float * 4), 86 | ('isAIControlled', c_int32), 87 | ('tyreContactPoint', c_float * 4 * 3), 88 | ('tyreContactNormal', c_float * 4 * 3), 89 | ('tyreContactHeading', c_float * 4 * 3), 90 | ('brakeBias', c_float), 91 | ('localVelocity', c_float * 3), 92 | 93 | ] 94 | 95 | class SPageFileGraphic(ctypes.Structure): 96 | _pack_ = 4 97 | _fields_ = [ 98 | ('packetId', c_int32), 99 | ('status', AC_STATUS), 100 | ('session', AC_SESSION_TYPE), 101 | ('currentTime', c_wchar * 15), 102 | ('lastTime', c_wchar * 15), 103 | ('bestTime', c_wchar * 15), 104 | ('split', c_wchar * 15), 105 | ('completedLaps', c_int32), 106 | ('position', c_int32), 107 | ('iCurrentTime', c_int32), 108 | ('iLastTime', c_int32), 109 | ('iBestTime', c_int32), 110 | ('sessionTimeLeft', c_float), 111 | ('distanceTraveled', c_float), 112 | ('isInPit', c_int32), 113 | ('currentSectorIndex', c_int32), 114 | ('lastSectorTime', c_int32), 115 | ('numberOfLaps', c_int32), 116 | ('tyreCompound', c_wchar * 33), 117 | ('replayTimeMultiplier', c_float), 118 | ('normalizedCarPosition', c_float), 119 | ('carCoordinates', c_float * 3), 120 | ('penaltyTime', c_float), 121 | ('flag', AC_FLAG_TYPE), 122 | ('idealLineOn', c_int32), 123 | ('isInPitLine', c_int32), 124 | ('surfaceGrip', c_float), 125 | ('mandatoryPitDone', c_int32), 126 | ('windSpeed', c_float), 127 | ('windDirection', c_float), 128 | 129 | ] 130 | 131 | class SPageFileStatic(ctypes.Structure): 132 | _pack_ = 4 133 | _fields_ = [ 134 | ('_smVersion', c_wchar * 15), 135 | ('_acVersion', c_wchar * 15), 136 | ('numberOfSessions', c_int32), 137 | ('numCars', c_int32), 138 | ('carModel', c_wchar * 33), 139 | ('track', c_wchar * 33), 140 | ('playerName', c_wchar * 33), 141 | ('playerSurname', c_wchar * 33), 142 | ('playerNick', c_wchar * 33), 143 | ('sectorCount', c_int32), 144 | ('maxTorque', c_float), 145 | ('maxPower', c_float), 146 | ('maxRpm', c_int32), 147 | ('maxFuel', c_float), 148 | ('suspensionMaxTravel', c_float * 4), 149 | ('tyreRadius', c_float * 4), 150 | ('maxTurboBoost', c_float), 151 | ('airTemp', c_float), 152 | ('roadTemp', c_float), 153 | ('penaltiesEnabled', c_int32), 154 | ('aidFuelRate', c_float), 155 | ('aidTireRate', c_float), 156 | ('aidMechanicalDamage', c_float), 157 | ('aidAllowTyreBlankets', c_int32), 158 | ('aidStability', c_float), 159 | ('aidAutoClutch', c_int32), 160 | ('aidAutoBlip', c_int32), 161 | ('hasDRS', c_int32), 162 | ('hasERS', c_int32), 163 | ('hasKERS', c_int32), 164 | ('kersMaxJ', c_float), 165 | ('engineBrakeSettingsCount', c_int32), 166 | ('ersPowerControllerCount', c_int32), 167 | ('trackSPlineLength', c_float), 168 | ('trackConfiguration', c_wchar * 33), 169 | ('ersMaxJ', c_float), 170 | ('isTimedRace', c_int32), 171 | ('hasExtraLap', c_int32), 172 | ('carSkin', c_wchar * 33), 173 | ('reversedGridPositions', c_int32), 174 | ('pitWindowStart', c_int32), 175 | ('pitWindowEnd', c_int32), 176 | 177 | ] 178 | 179 | class SimInfo: 180 | def __init__(self): 181 | self._acpmf_physics = mmap.mmap(0, ctypes.sizeof(SPageFilePhysics), "acpmf_physics") 182 | self._acpmf_graphics = mmap.mmap(0, ctypes.sizeof(SPageFileGraphic), "acpmf_graphics") 183 | self._acpmf_static = mmap.mmap(0, ctypes.sizeof(SPageFileStatic), "acpmf_static") 184 | self.physics = SPageFilePhysics.from_buffer(self._acpmf_physics) 185 | self.graphics = SPageFileGraphic.from_buffer(self._acpmf_graphics) 186 | self.static = SPageFileStatic.from_buffer(self._acpmf_static) 187 | 188 | def close(self): 189 | self._acpmf_physics.close() 190 | self._acpmf_graphics.close() 191 | self._acpmf_static.close() 192 | 193 | def __del__(self): 194 | self.close() 195 | 196 | info = SimInfo() 197 | 198 | def demo(): 199 | import time 200 | 201 | for _ in range(400): 202 | print(info.static.track, info.graphics.tyreCompound, info.graphics.currentTime, 203 | info.physics.rpms, info.graphics.currentTime, info.static.maxRpm, list(info.physics.tyreWear)) 204 | time.sleep(0.1) 205 | 206 | def do_test(): 207 | for struct in info.static, info.graphics, info.physics: 208 | print(struct.__class__.__name__) 209 | for field, type_spec in struct._fields_: 210 | value = getattr(struct, field) 211 | if not isinstance(value, (str, float, int)): 212 | value = list(value) 213 | print(" {} -> {} {}".format(field, type(value), value)) 214 | 215 | if __name__ == '__main__': 216 | do_test() 217 | demo() 218 | -------------------------------------------------------------------------------- /apps/python/traces/drawables.py: -------------------------------------------------------------------------------- 1 | import ac 2 | import acsys 3 | 4 | from collections import deque 5 | 6 | from ac_gl_utils import Point 7 | from ac_gl_utils import Line 8 | from ac_gl_utils import Triangle 9 | from ac_gl_utils import Quad 10 | 11 | 12 | class Trace: 13 | """Driver input trace drawable. 14 | 15 | Args: 16 | cfg (obj:Config): Object for app configuration. 17 | ac_global_data (obj:ACGlobalData): Object to retrieve Assetto Corsa 18 | data that is non-car specific. 19 | color (tuple): r,g,b,a on 0 to 1 scale. 20 | """ 21 | def __init__(self, cfg, ac_global_data, color): 22 | self.cfg = cfg 23 | self.ac_global_data = ac_global_data 24 | 25 | self.time_window = self.cfg.trace_time_window 26 | self.sample_rate = self.cfg.trace_sample_rate 27 | self.sample_size = self.time_window * self.sample_rate 28 | 29 | self.color = color 30 | self.thickness = self.cfg.trace_thickness 31 | self.half_thickness = self.thickness / 2 32 | 33 | # Trace line starting point 34 | self.graph_origin = Point( 35 | self.cfg.app_height * self.cfg.app_padding + self.half_thickness, 36 | self.cfg.app_height * (1 - self.cfg.app_padding) - self.half_thickness) 37 | 38 | # Trace graph dimensions 39 | self.graph_height = self.cfg.app_height * (1 - 2 * self.cfg.app_padding) - self.thickness 40 | self.graph_width = self.cfg.app_height * 2.5 - self.thickness 41 | 42 | # Set up render queue and points deques. 43 | # self.render_queue is a deque of quads, iterated over to draw. 44 | # (2*sample_size - 1) deque length because there are: 45 | # N (sample size) data points and N-1 connecting lines between points 46 | self.render_queue = deque(maxlen=(2 * self.sample_size - 1)) 47 | 48 | # self.points is a deque of data points, the current and the lag data point. 49 | # This is used in calculating the quad connecting the data points. 50 | self.points = deque(maxlen=2) 51 | 52 | def update(self, data_point): 53 | """Update trace render queue. 54 | 55 | Args: 56 | data_point (float): New point to add to the trace. 57 | """ 58 | if self.ac_global_data.replay_time_multiplier > 0: 59 | # Update traces only if sim time multiplier is positive 60 | 61 | # Offset all points by one 62 | for point in self.points: 63 | point.x -= self.graph_width / (self.sample_size - 1) 64 | 65 | # Move all quads in render queue left by one unit 66 | for quad in self.render_queue: 67 | quad.points[0].x -= self.graph_width / (self.sample_size - 1) 68 | quad.points[1].x -= self.graph_width / (self.sample_size - 1) 69 | quad.points[2].x -= self.graph_width / (self.sample_size - 1) 70 | quad.points[3].x -= self.graph_width / (self.sample_size - 1) 71 | 72 | # Add new point 73 | p = Point(self.graph_origin.x + self.graph_width, 74 | self.graph_origin.y - (data_point * self.graph_height)) 75 | self.points.append(p.copy()) 76 | 77 | p_lag = self.points[0] 78 | # Make connecting quad if previous point exists 79 | # Checked by seeing if points deque is length of two... 80 | if len(self.points) != 2: 81 | pass 82 | elif (p.x > p_lag.x) == (p.y > p_lag.y): 83 | # If x and y are both greater or smaller than lag x and y 84 | p1 = Point(p_lag.x + self.half_thickness, 85 | p_lag.y - self.half_thickness) 86 | p2 = Point(p.x + self.half_thickness, 87 | p.y - self.half_thickness) 88 | p3 = Point(p.x - self.half_thickness, 89 | p.y + self.half_thickness) 90 | p4 = Point(p_lag.x - self.half_thickness, 91 | p_lag.y + self.half_thickness) 92 | # Points of a triangle/quad must be passed in CCW order, 93 | # as this defines the front facing side. 94 | # Clockwise is back face, which gets culled. 95 | conn_quad = Quad(p4, p3, p2, p1) 96 | self.render_queue.append(conn_quad.copy()) 97 | else: 98 | p1 = Point(p_lag.x - self.half_thickness, 99 | p_lag.y - self.half_thickness) 100 | p2 = Point(p.x - self.half_thickness, 101 | p.y - self.half_thickness) 102 | p3 = Point(p.x + self.half_thickness, 103 | p.y + self.half_thickness) 104 | p4 = Point(p_lag.x + self.half_thickness, 105 | p_lag.y + self.half_thickness) 106 | conn_quad = Quad(p4, p3, p2, p1) 107 | self.render_queue.append(conn_quad.copy()) 108 | 109 | # Make a square around the data point 110 | p1 = Point(p.x - self.half_thickness, 111 | p.y - self.half_thickness) 112 | p2 = Point(p.x + self.half_thickness, 113 | p.y - self.half_thickness) 114 | p3 = Point(p.x + self.half_thickness, 115 | p.y + self.half_thickness) 116 | p4 = Point(p.x - self.half_thickness, 117 | p.y + self.half_thickness) 118 | square = Quad(p4, p3, p2, p1) 119 | self.render_queue.append(square.copy()) 120 | 121 | elif self.ac_global_data.replay_time_multiplier == 0: 122 | # If sim time is paused, dont update traces, skip. 123 | pass 124 | else: 125 | # If sim time multiplier is negative, clear traces to empty defaults 126 | self.points.clear() 127 | self.render_queue.clear() 128 | 129 | def draw(self): 130 | """Draw trace object""" 131 | set_color(self.color) 132 | try: 133 | for quad in self.render_queue: 134 | ac.glBegin(acsys.GL.Quads) 135 | ac.glVertex2f(quad.points[0].x, quad.points[0].y) 136 | ac.glVertex2f(quad.points[1].x, quad.points[1].y) 137 | ac.glVertex2f(quad.points[2].x, quad.points[2].y) 138 | ac.glVertex2f(quad.points[3].x, quad.points[3].y) 139 | ac.glEnd() 140 | except Exception as e: 141 | ac.log("{app_name} - Error: \n{error}".format(app_name=self.cfg.app_name, error=e)) 142 | 143 | 144 | class PedalBar: 145 | """Driver pedal input bar drawable. 146 | 147 | Args: 148 | cfg (obj:Config): App configuration. 149 | origin_x (float): x origin point on full app scale 150 | to start drawing the pedal bar from. 151 | color (tuple): r,g,b,a on a 0-1 scale. 152 | """ 153 | def __init__(self, cfg, origin_x, color): 154 | self.cfg = cfg 155 | self.color = color 156 | 157 | self.origin = Point(origin_x * self.cfg.app_scale, 158 | 450 * self.cfg.app_scale) 159 | self.width = self.cfg.app_height * self.cfg.app_padding 160 | # Height will be multiplied by pedal input. 161 | self.full_height = self.cfg.app_height * (1- (self.cfg.app_padding * 2)) 162 | 163 | self.pedal_input = 0 164 | 165 | def update(self, pedal_input): 166 | """Update pedal input data. 167 | 168 | Args: 169 | pedal_input (float): Pedal input data to draw. 170 | """ 171 | self.pedal_input = pedal_input 172 | 173 | def draw(self): 174 | """Draw pedal bar""" 175 | set_color(self.color) 176 | ac.glBegin(acsys.GL.Quads) 177 | ac.glVertex2f(self.origin.x, 178 | self.origin.y) 179 | ac.glVertex2f(self.origin.x + self.width, 180 | self.origin.y) 181 | ac.glVertex2f(self.origin.x + self.width, 182 | self.origin.y - (self.full_height * self.pedal_input)) 183 | ac.glVertex2f(self.origin.x, 184 | self.origin.y - (self.full_height * self.pedal_input)) 185 | ac.glEnd() 186 | 187 | 188 | class SteeringWheel: 189 | """Driver steering wheel input indicator drawable. 190 | 191 | Args: 192 | cfg (obj:Config): App configuration. 193 | color (tuple): r,g,b,a on a 0-1 scale. 194 | """ 195 | def __init__(self, cfg, color): 196 | self.cfg = cfg 197 | self.color = color 198 | 199 | # Center of rotation coordinates of the steering wheel. 200 | self.origin = Point(1935 * self.cfg.app_scale, 201 | 300 * self.cfg.app_scale) 202 | 203 | # Radius to inside and outside of steering wheel rim. 204 | self.outer_radius = 150 * self.cfg.app_scale 205 | self.ratio_inner_outer_radius = 112 / 150 206 | self.inner_radius = self.outer_radius * self.ratio_inner_outer_radius 207 | 208 | # Build initial renderqueue based on straight wheel. 209 | # This will get rotated by updating the steering wheel angle. 210 | self.center_p_outer = Point(self.origin.x, 211 | self.origin.y - self.outer_radius) 212 | self.center_p_inner = Point(self.origin.x, 213 | self.origin.y - self.inner_radius) 214 | 215 | # Built on the basis of one starting line connecting the inside and 216 | # outside of the rim at the center. Copy the center line with rotation offsets, 217 | # and build a base renderqueue of quads from it. 218 | self.start_line = Line(self.center_p_inner, self.center_p_outer) 219 | 220 | self.line_list = [] 221 | self.base_quads = [] 222 | self.offsets = [-0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2] 223 | for i, offset in enumerate(self.offsets): 224 | line = self.start_line.copy() 225 | line.rotate_rad(offset, self.origin) 226 | self.line_list.append(line) 227 | 228 | if i == 0: 229 | pass 230 | else: 231 | line_lag = self.line_list[i-1] 232 | 233 | p1 = Point(line.points[0].x, line.points[0].y) 234 | p2 = Point(line.points[1].x, line.points[1].y) 235 | p3 = Point(line_lag.points[1].x, line_lag.points[1].y) 236 | p4 = Point(line_lag.points[0].x, line_lag.points[0].y) 237 | quad = Quad(p1, p2, p3, p4) 238 | self.base_quads.append(quad) 239 | 240 | # Initialize empty renderqueue 241 | self.render_queue = [] 242 | 243 | def update(self, angle): 244 | """Update steering wheel indicator. 245 | 246 | Args: 247 | angle (float): Steering wheel angle in radians. 248 | """ 249 | _render_queue = [] 250 | 251 | for quad in self.base_quads: 252 | new_quad = quad.copy() 253 | new_quad.rotate_rad(angle, self.origin) 254 | _render_queue.append(new_quad) 255 | 256 | self.render_queue = _render_queue 257 | 258 | def draw(self): 259 | """Draw steering wheel indicator""" 260 | set_color(self.color) 261 | for quad in self.render_queue: 262 | ac.glBegin(acsys.GL.Quads) 263 | ac.glVertex2f(quad.points[0].x, quad.points[0].y) 264 | ac.glVertex2f(quad.points[1].x, quad.points[1].y) 265 | ac.glVertex2f(quad.points[2].x, quad.points[2].y) 266 | ac.glVertex2f(quad.points[3].x, quad.points[3].y) 267 | ac.glEnd() 268 | 269 | 270 | def set_color(rgba): 271 | """Apply RGBA color for GL drawing. 272 | 273 | Agrs: 274 | rgba (tuple): r,g,b,a on a 0-1 scale. 275 | """ 276 | ac.glColor4f(rgba[0], rgba[1], rgba[2], rgba[3]) 277 | -------------------------------------------------------------------------------- /apps/python/traces/ac_gl_utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | # The classes below are used as building blocks for the OpenGL rendering of vector graphics in Assetto Corsa. 4 | # All classes are based on the two dimensional cartesian coordinate system. 5 | 6 | class Point: 7 | """A point in a 2D cartesian coordinate system. 8 | 9 | Each point is described by an X and a Y coordinate. 10 | 11 | Args: 12 | x (float): x coordinate. 13 | optional, defaults to 0 14 | y (float): y coordinate. 15 | optional, defaults to 0 16 | """ 17 | def __init__(self, x=0, y=0): 18 | self.x = float(x) 19 | self.y = float(y) 20 | 21 | def add(self, p): 22 | """Add value to Point. 23 | 24 | Args: 25 | p (obj:Point/float): Value to add to the Point x,y. 26 | Can be Point obj to add different values to x and y, 27 | or a float to add same value to both x and y. 28 | """ 29 | # Check if p is a Point object 30 | # If true, add x,y of p to respective x,y of Point 31 | # If false, add p to both x and y of Point 32 | if not isinstance(p, Point): p = Point(p, p) 33 | self.x += p.x 34 | self.y += p.y 35 | 36 | def subtract(self, p): 37 | """Subtract value from Point. 38 | 39 | Args: 40 | p (obj:Point/float): Value to subtract from the Point x,y. 41 | Can be Point obj to subtract different values from x and y, 42 | or a float to subtract same value from both x and y. 43 | """ 44 | # Check if p is a Point object 45 | # If true, subtract x,y of p from respective x,y of Point 46 | # If false, subtract p from both x and y of Point 47 | if not isinstance(p, Point): p = Point(p, p) 48 | self.x -= p.x 49 | self.y -= p.y 50 | 51 | def multiply(self, val): 52 | """Multiply Point by value. 53 | 54 | Args: 55 | p (obj:Point/float): Value to multiply Point x,y with. 56 | Can be Point obj to multiply different values with x and y, 57 | or a float to multiply same value with both x and y. 58 | """ 59 | # Check if p is a Point object 60 | # If true, multiply x,y of p with respective x,y of Point 61 | # If false, multiply p with both x and y of Point 62 | if not isinstance(p, Point): p = Point(p, p) 63 | self.x *= p.x 64 | self.y *= p.y 65 | 66 | def divide(self, val): 67 | """Divide Point by value. 68 | 69 | Args: 70 | p (obj:Point/float): Value to divide Point x,y by. 71 | Can be Point obj to divide different values with x and y, 72 | or a float to divide same value with both x and y. 73 | """ 74 | # Check if p is a Point object 75 | # If true, multiply x,y of p with respective x,y of Point 76 | # If false, multiply p with both x and y of Point 77 | if not isinstance(p, Point): p = Point(p, p) 78 | self.x /= p.x 79 | self.y /= p.y 80 | 81 | def rotate_rad(self, angle, cor=0): 82 | """Rotate Point in positive counterclockwise direction. 83 | 84 | Rotation is done in radians, optionally around a specified 85 | center of rotation. If no center of rotation is specified, 86 | Point is rotated around origin (0,0). 87 | 88 | Args: 89 | angle (float): Rotation in radians. 90 | cor (obj:Point): Center of rotation (x,y). 91 | """ 92 | # Calculate trig functions only once beforehand. 93 | c = math.cos(angle) 94 | s = math.sin(angle) 95 | 96 | self._rotate(c, s, cor) 97 | 98 | def rotate_deg(self, angle, cor=0): 99 | """Rotate Point in positive counterclockwise direction. 100 | 101 | Rotation is done in degrees, optionally around a specified 102 | center of rotation. If no center of rotation is specified, 103 | Point is rotated around origin (0,0). 104 | 105 | Args: 106 | angle (float): Degrees of rotation. 107 | cor (obj:Point): Center of rotation (x,y). 108 | """ 109 | # Convert degrees to radians 110 | angle = angle * math.pi / 180 111 | 112 | # Calculate trig functions 113 | c = math.cos(angle) 114 | s = math.sin(angle) 115 | 116 | self._rotate(c, s, cor) 117 | 118 | def _rotate(self, c, s, cor=0): 119 | """Rotate point in positive counterclockwise direction. 120 | 121 | Args: 122 | c (float): cosine of desired rotation angle. 123 | s (float): sine of desired rotation angle. 124 | cor (obj:Point): center of rotation (x,y). 125 | """ 126 | # Separate calculation of rotation from trig functions 127 | # because sine and cosine don't change for same rotation angle 128 | # Wasteful to recalculate mutiple times when rotating e.g. a quad. 129 | p = Point(self.x, self.y) 130 | 131 | # Subtract center of rotation coords from Point, 132 | # to rotate Point around origin (0,0). 133 | # After rotation is done, add back center of rotation coords. 134 | p.subtract(cor) 135 | 136 | # Positive Counterclockwise Rotation 137 | self.x = p.x * c - p.y * s 138 | self.y = p.x * s + p.y * c 139 | self.add(cor) 140 | 141 | def copy(self): 142 | """Return a copy of object.""" 143 | return Point(self.x, self.y) 144 | 145 | 146 | class Line: 147 | """A line in a 2D cartesian coordinate system. 148 | 149 | Each line is described by a set of two points. 150 | 151 | Args: 152 | p1 (obj:Point): Start point of the line. 153 | p2 (obj:Point): End point of the line. 154 | """ 155 | def __init__(self, p1=Point(), p2=Point()): 156 | self.points = [p1, p2] 157 | 158 | def add(self, p): 159 | """Add value to all points in Line. 160 | 161 | Args: 162 | p (obj:Point/float): Value to add to points in Line. 163 | Can be Point obj to add different values to x and y of points, 164 | or a float to add same value to both x and y of points. 165 | """ 166 | for point in self.points: 167 | point.add(p) 168 | 169 | def subtract(self, p): 170 | """Subtract value from all points in Line. 171 | 172 | Args: 173 | p (obj:Point/float): Value to subtract from points in Line. 174 | Can be Point obj to subtract different values from x and y of points, 175 | or a float to subtract same value from both x and y of points. 176 | """ 177 | for point in self.points: 178 | point.subtract(p) 179 | 180 | def multiply(self, p): 181 | """Multiply all points in Line with value. 182 | 183 | Args: 184 | p (obj:Point/float): Value to multiply points in Line with. 185 | Can be Point obj to multiply different values with x and y of points, 186 | or a float to multiply same value with both x and y of points. 187 | """ 188 | for point in self.points: 189 | point.multiply(p) 190 | 191 | def divide(self, p): 192 | """Divide all points in Line by value. 193 | 194 | Args: 195 | p (obj:Point/float): Value to divide points in Line by. 196 | Can be Point obj to divide x and y of points with different values, 197 | or a float to divide both x and y of points by the same value. 198 | """ 199 | for point in self.points: 200 | point.divide(p) 201 | 202 | def rotate_rad(self, angle, cor=0): 203 | """Rotate Line in positive counterclockwise direction. 204 | 205 | Rotation is done in radians, optionally around a specified 206 | center of rotation. If no center of rotation is specified, 207 | the Line is rotated around origin (0,0). 208 | 209 | Args: 210 | angle (float): Rotation in radians. 211 | cor (obj:Point): Center of rotation (x,y) 212 | """ 213 | # Calculate trig functions only once beforehand. 214 | c = math.cos(angle) 215 | s = math.sin(angle) 216 | 217 | for points in self.points: 218 | points._rotate(c, s, cor) 219 | 220 | def rotate_deg(self, angle, cor=0): 221 | """Rotate Line in positive counterclockwise direction. 222 | 223 | Rotation is done in degrees, optionally around a specified 224 | center of rotation. If no center of rotation is specified, 225 | the Line is rotated around origin (0,0). 226 | 227 | Args: 228 | angle (float): Rotation in degrees. 229 | cor (obj:Point): Center of rotation (x,y) 230 | """ 231 | # Convert degrees to radians 232 | angle = angle * math.pi / 180 233 | 234 | # Calculate trig functions 235 | c = math.cos(angle) 236 | s = math.sin(angle) 237 | 238 | for points in self.points: 239 | points._rotate(c, s, cor) 240 | 241 | def copy(self): 242 | """Return a copy of object.""" 243 | return Line(self.points[0].copy(), self.points[1].copy()) 244 | 245 | 246 | class Triangle: 247 | """A triangle in a 2D cartesian coordinate system. 248 | 249 | Each triangle is described by a set of three points. 250 | 251 | Args: 252 | p1 (obj:Point): First point of Triangle. 253 | p2 (obj:Point): Second point of Triangle. 254 | p3 (obj:Point): Third point of Triangle. 255 | """ 256 | def __init__(self, 257 | p1=Point(), 258 | p2=Point(), 259 | p3=Point()): 260 | self.points = [p1, p2, p3] 261 | 262 | def add(self, p): 263 | """Add value to all points in Triangle. 264 | 265 | Args: 266 | p (obj:Point/float): Value to add to points in Triangle. 267 | Can be Point obj to add different values to x and y of points, 268 | or a float to add same value to both x and y of points. 269 | """ 270 | for point in self.points: 271 | point.add(p) 272 | 273 | def subtract(self, p): 274 | """Subtract value from all points in Triangle. 275 | 276 | Args: 277 | p (obj:Point/float): Value to subtract from points in Triangle. 278 | Can be Point obj to subtract different values from x and y of points, 279 | or a float to subtract same value from both x and y of points. 280 | """ 281 | for point in self.points: 282 | point.subtract(p) 283 | 284 | def multiply(self, p): 285 | """Multiply all points in Triangle with value. 286 | 287 | Args: 288 | p (obj:Point/float): Value to multiply points in Triangle with. 289 | Can be Point obj to multiply different values with x and y of points, 290 | or a float to multiply same value with both x and y of points. 291 | """ 292 | for point in self.points: 293 | point.multiply(p) 294 | 295 | def divide(self, p): 296 | """Divide all points in Triangle by value. 297 | 298 | Args: 299 | p (obj:Point/float): Value to divide points in Triangle by. 300 | Can be Point obj to divide x and y of points with different values, 301 | or a float to divide both x and y of points by the same value. 302 | """ 303 | for point in self.points: 304 | point.divide(p) 305 | 306 | def rotate_rad(self, angle, cor=0): 307 | """Rotate Triangle in positive counterclockwise direction. 308 | 309 | Rotation is done in radians, optionally around a specified 310 | center of rotation. If no center of rotation is specified, 311 | the Triangle is rotated around origin (0,0). 312 | 313 | Args: 314 | angle (float): Rotation in radians. 315 | cor (obj:Point): Center of rotation (x,y) 316 | """ 317 | # Calculate trig functions only once beforehand. 318 | c = math.cos(angle) 319 | s = math.sin(angle) 320 | 321 | for points in self.points: 322 | points._rotate(c, s, cor) 323 | 324 | def rotate_deg(self, angle, cor=0): 325 | """Rotate Triangle in positive counterclockwise direction. 326 | 327 | Rotation is done in degrees, optionally around a specified 328 | center of rotation. If no center of rotation is specified, 329 | the Triangle is rotated around origin (0,0). 330 | 331 | Args: 332 | angle (float): Rotation in degrees. 333 | cor (obj:Point): Center of rotation (x,y) 334 | """ 335 | # Convert degrees to radians 336 | angle = angle * math.pi / 180 337 | 338 | # Calculate trig functions 339 | c = math.cos(angle) 340 | s = math.sin(angle) 341 | 342 | for points in self.points: 343 | points._rotate(c, s, cor) 344 | 345 | def copy(self): 346 | """Return a copy of object.""" 347 | return Triangle(self.points[0].copy(), 348 | self.points[1].copy(), 349 | self.points[2].copy()) 350 | 351 | 352 | class Quad: 353 | """A quad in a 2D cartesian coordinate system. 354 | 355 | Each quad is described by a set of four points. 356 | 357 | Args: 358 | p1 (obj:Point): First point of Quad. 359 | p2 (obj:Point): Second point of Quad. 360 | p3 (obj:Point): Third point of Quad. 361 | p4 (obj:Point): Fourth point of Quad 362 | """ 363 | def __init__(self, 364 | p1=Point(), 365 | p2=Point(), 366 | p3=Point(), 367 | p4=Point()): 368 | self.points = [p1, p2, p3, p4] 369 | 370 | def add(self, p): 371 | """Add value to all points in Quad. 372 | 373 | Args: 374 | p (obj:Point/float): Value to add to points in Quad. 375 | Can be Point obj to add different values to x and y of points, 376 | or a float to add same value to both x and y of points. 377 | """ 378 | for point in self.points: 379 | point.add(p) 380 | 381 | def subtract(self, p): 382 | """Subtract value from all points in Quad. 383 | 384 | Args: 385 | p (obj:Point/float): Value to subtract from points in Quad. 386 | Can be Point obj to subtract different values from x and y of points, 387 | or a float to subtract same value from both x and y of points. 388 | """ 389 | for point in self.points: 390 | point.subtract(p) 391 | 392 | def multiply(self, p): 393 | """Multiply all points in Quad with value. 394 | 395 | Args: 396 | p (obj:Point/float): Value to multiply points in Quad with. 397 | Can be Point obj to multiply different values with x and y of points, 398 | or a float to multiply same value with both x and y of points. 399 | """ 400 | for point in self.points: 401 | point.multiply(p) 402 | 403 | def divide(self, p): 404 | """Divide all points in Quad by value. 405 | 406 | Args: 407 | p (obj:Point/float): Value to divide points in Quad by. 408 | Can be Point obj to divide x and y of points with different values, 409 | or a float to divide both x and y of points by the same value. 410 | """ 411 | for point in self.points: 412 | point.divide(p) 413 | 414 | def rotate_rad(self, angle, cor=0): 415 | """Rotate Quad in positive counterclockwise direction. 416 | 417 | Rotation is done in radians, optionally around a specified 418 | center of rotation. If no center of rotation is specified, 419 | the Quad is rotated around origin (0,0). 420 | 421 | Args: 422 | angle (float): Rotation in radians. 423 | cor (obj:Point): Center of rotation (x,y) 424 | """ 425 | # Calculate trig functions only once beforehand. 426 | c = math.cos(angle) 427 | s = math.sin(angle) 428 | 429 | for points in self.points: 430 | points._rotate(c, s, cor) 431 | 432 | def rotate_deg(self, angle, cor=0): 433 | """Rotate Quad in positive counterclockwise direction. 434 | 435 | Rotation is done in degrees, optionally around a specified 436 | center of rotation. If no center of rotation is specified, 437 | the Quad is rotated around origin (0,0). 438 | 439 | Args: 440 | angle (float): Rotation in degrees. 441 | cor (obj:Point): Center of rotation (x,y) 442 | """ 443 | # Convert degrees to radians 444 | angle = angle * math.pi / 180 445 | 446 | # Calculate trig functions 447 | c = math.cos(angle) 448 | s = math.sin(angle) 449 | 450 | for points in self.points: 451 | points._rotate(c, s, cor) 452 | 453 | def copy(self): 454 | """Return a copy of object.""" 455 | return Quad(self.points[0].copy(), 456 | self.points[1].copy(), 457 | self.points[2].copy(), 458 | self.points[3].copy()) 459 | --------------------------------------------------------------------------------