├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── cpp ├── scripthookvpy3k.sln ├── scripthookvpy3k.vcxproj ├── scripthookvpy3k.vcxproj.filters └── src │ ├── main.cpp │ ├── natives.i │ ├── natives_wrap.cxx │ ├── natives_wrap.h │ ├── wrapper.cpp │ └── wrapper.h ├── python ├── gta │ ├── __init__.py │ ├── enums.py │ ├── events.py │ ├── exceptions.py │ ├── requires │ │ ├── __init__.py │ │ ├── ped.py │ │ └── player.py │ ├── ui │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── menu.py │ │ └── primitive.py │ └── utils.py ├── gta_native.py └── scripts │ ├── helper.py │ ├── metadata.py │ ├── running.py │ ├── ui.py │ ├── vehicle_color.py │ ├── wanted.py │ └── weather_time.py ├── sdk └── .gitignore ├── swig └── .gitignore └── tools ├── generate.py └── simulate.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore SDK 2 | sdk/* 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Rr]elease/ 12 | build/ 13 | [Bb]in/ 14 | [Oo]bj/ 15 | 16 | # MSTest test Results 17 | [Tt]est[Rr]esult*/ 18 | [Bb]uild[Ll]og.* 19 | 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.tmp_proj 36 | *.log 37 | *.vspscc 38 | *.vssscc 39 | .builds 40 | *.pidb 41 | *.log 42 | *.scc 43 | 44 | # Visual C++ cache files 45 | ipch/ 46 | *.aps 47 | *.ncb 48 | *.opensdf 49 | *.sdf 50 | *.cachefile 51 | 52 | # Visual Studio profiler 53 | *.psess 54 | *.vsp 55 | *.vspx 56 | 57 | # Guidance Automation Toolkit 58 | *.gpState 59 | 60 | # Others 61 | *.Cache 62 | ~$* 63 | *~ 64 | *.pfx 65 | 66 | # Windows image file caches 67 | Thumbs.db 68 | ehthumbs.db 69 | 70 | # Folder config file 71 | Desktop.ini 72 | 73 | # Recycle Bin used on file shares 74 | $RECYCLE.BIN/ 75 | 76 | # Mac desktop service store files 77 | .DS_Store 78 | 79 | # Compiled Object files 80 | *.slo 81 | *.lo 82 | *.o 83 | *.obj 84 | 85 | # Precompiled Headers 86 | *.gch 87 | *.pch 88 | 89 | # Compiled Dynamic libraries 90 | *.so 91 | *.dylib 92 | *.dll 93 | 94 | # Fortran module files 95 | *.mod 96 | 97 | # Compiled Static libraries 98 | *.lai 99 | *.la 100 | *.a 101 | *.lib 102 | 103 | # Executables 104 | *.exe 105 | *.out 106 | *.app 107 | 108 | # Byte-compiled / optimized / DLL files 109 | __pycache__/ 110 | *.py[cod] 111 | *$py.class 112 | 113 | # Distribution / packaging 114 | .Python 115 | env/ 116 | build/ 117 | develop-eggs/ 118 | dist/ 119 | downloads/ 120 | eggs/ 121 | .eggs/ 122 | lib/ 123 | lib64/ 124 | parts/ 125 | sdist/ 126 | var/ 127 | *.egg-info/ 128 | .installed.cfg 129 | *.egg 130 | 131 | # PyInstaller 132 | # Usually these files are written by a python script from a template 133 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 134 | *.manifest 135 | *.spec 136 | 137 | # Installer logs 138 | pip-log.txt 139 | pip-delete-this-directory.txt 140 | 141 | # Unit test / coverage reports 142 | htmlcov/ 143 | .tox/ 144 | .coverage 145 | .coverage.* 146 | .cache 147 | nosetests.xml 148 | coverage.xml 149 | *,cover 150 | 151 | # Translations 152 | *.mo 153 | *.pot 154 | 155 | # Django stuff: 156 | *.log 157 | 158 | # Sphinx documentation 159 | docs/_build/ 160 | 161 | # PyBuilder 162 | target/ 163 | 164 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 165 | 166 | *.iml 167 | 168 | ## Directory-based project format: 169 | .idea/ 170 | # if you remove the above rule, at least ignore the following: 171 | 172 | # User-specific stuff: 173 | # .idea/workspace.xml 174 | # .idea/tasks.xml 175 | # .idea/dictionaries 176 | 177 | # Sensitive or high-churn files: 178 | # .idea/dataSources.ids 179 | # .idea/dataSources.xml 180 | # .idea/sqlDataSources.xml 181 | # .idea/dynamic.xml 182 | # .idea/uiDesigner.xml 183 | 184 | # Gradle: 185 | # .idea/gradle.xml 186 | # .idea/libraries 187 | 188 | # Mongo Explorer plugin: 189 | # .idea/mongoSettings.xml 190 | 191 | ## File-based project format: 192 | *.ipr 193 | *.iws 194 | 195 | ## Plugin-specific files: 196 | 197 | # IntelliJ 198 | /out/ 199 | 200 | # mpeltonen/sbt-idea plugin 201 | .idea_modules/ 202 | 203 | # JIRA plugin 204 | atlassian-ide-plugin.xml 205 | 206 | # Crashlytics plugin (for Android Studio and IntelliJ) 207 | com_crashlytics_export_strings.xml 208 | crashlytics.properties 209 | crashlytics-build.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lennart Grahl 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Script Hook V Py3k 2 | This is an ASI plugin for Grand Theft Auto V that allows you to execute Python 3 scripts 3 | in game by using the Script Hook V from Alexander Blade. 4 | 5 | ## Features 6 | * All [native functions](http://www.dev-c.com/nativedb/) accessible 7 | * Scripts run as lightweight asynchronous tasks 8 | * High level [requirement functions](/python/gta/requires) can be chained and waited for 9 | * All the [fancy Python packages out there](https://warehouse.python.org) can be used 10 | and... 11 | * Dependencies will be installed automatically 12 | 13 | ## Download 14 | Prebuilt binaries can be found on the [releases](../../releases) 15 | page. 16 | 17 | ## Installation 18 | 1. Install the [Script Hook V](http://www.dev-c.com/gtav/scripthookv/) 19 | 2. Install [Python 3.5.0 for Windows x64](https://www.python.org/ftp/python/3.5.0/python-3.5.0-amd64.exe) 20 | 3. Copy the contents of the downloaded archive into your GTA V game folder 21 | 22 | ## Writing Scripts 23 | To get started on writing scripts, head over to [this wiki page](../../wiki/Writing-Scripts). 24 | 25 | ## Contributing 26 | All contributions are warmly welcomed. Below are a few hints to the entry points of the 27 | code and a link to our to do list. 28 | 29 | ### Entry Points 30 | * ``Py3kWrapperStart`` in [wrapper.cpp](/cpp/src/wrapper.cpp) is the entry point for the 31 | C++ part of the plugin 32 | * ``_init`` in [the gta module](/python/gta/__init__.py) is the entry point for the 33 | Python part of the plugin 34 | 35 | ### Todo 36 | See [TODO.md](/TODO.md). 37 | 38 | ## Building 39 | If you want to build the ASI plugin yourself, you'll need: 40 | 41 | 1. Visual Studio 2013 42 | 2. The [Script Hook V SDK](http://www.dev-c.com/gtav/scripthookv/) which has to be 43 | extracted into [/sdk](/sdk) after downloading 44 | 3. [SWIG](http://sourceforge.net/projects/swig/files/swigwin/) Version >= 3.0.5 which has 45 | to be extracted into [/swig](/swig) after downloading 46 | 4. **Python 3.5.0 for AMD64/EM64T/x64**. Using the x86 version will not work! 47 | 5. Open the project file and build the solution in *Release* configuration 48 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To Do 2 | 3 | Ordered as of priority. 4 | 5 | ## List 6 | 7 | * UI package 8 | * Console 9 | - `reload`: Reloads Python and all scripts 10 | - `start`: Start a script 11 | - `stop`: Stop a script 12 | - `install`: Installs a script/package 13 | - `python`: Runs an interactive Python interpreter 14 | * gta_native wrapper co-routines that simplify usage 15 | -------------------------------------------------------------------------------- /cpp/scripthookvpy3k.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "scripthookvpy3k", "scripthookvpy3k.vcxproj", "{8D82F34A-1D64-465B-84B1-37F89AD3D20B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8D82F34A-1D64-465B-84B1-37F89AD3D20B}.Debug|x64.ActiveCfg = Release|x64 15 | {8D82F34A-1D64-465B-84B1-37F89AD3D20B}.Debug|x64.Build.0 = Release|x64 16 | {8D82F34A-1D64-465B-84B1-37F89AD3D20B}.Release|x64.ActiveCfg = Release|x64 17 | {8D82F34A-1D64-465B-84B1-37F89AD3D20B}.Release|x64.Build.0 = Release|x64 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /cpp/scripthookvpy3k.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | x64 7 | 8 | 9 | Release 10 | x64 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {8D82F34A-1D64-465B-84B1-37F89AD3D20B} 34 | Win32Proj 35 | scripthookvpy3k 36 | scripthookvpy3k 37 | 38 | 39 | 40 | DynamicLibrary 41 | true 42 | v120 43 | MultiByte 44 | 45 | 46 | DynamicLibrary 47 | false 48 | v120 49 | true 50 | MultiByte 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | true 64 | .asi 65 | bin\$(Configuration)\ 66 | tmp\$(Configuration)\ 67 | 68 | 69 | false 70 | .asi 71 | bin\$(Configuration)\ 72 | tmp\$(Configuration)\ 73 | 74 | 75 | 76 | 77 | 78 | Level3 79 | Disabled 80 | WIN32;_DEBUG;_WINDOWS;_USRDLL;ScriptHookVPy3k_EXPORTS;%(PreprocessorDefinitions) 81 | MultiThreadedDebug 82 | C:\Python35\include;%(AdditionalIncludeDirectories) 83 | 84 | 85 | Windows 86 | true 87 | C:\Python35\libs;%(AdditionalLibraryDirectories) 88 | 89 | 90 | 91 | 92 | Level3 93 | 94 | 95 | MaxSpeed 96 | true 97 | true 98 | WIN32;NDEBUG;_WINDOWS;_USRDLL;ScriptHookVPy3k_EXPORTS;%(PreprocessorDefinitions) 99 | MultiThreaded 100 | Fast 101 | C:\Python35\include;%(AdditionalIncludeDirectories) 102 | 103 | 104 | Windows 105 | false 106 | true 107 | true 108 | 109 | 110 | C:\Python35\libs;%(AdditionalLibraryDirectories) 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /cpp/scripthookvpy3k.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | include 7 | 8 | 9 | include 10 | 11 | 12 | include 13 | 14 | 15 | include 16 | 17 | 18 | 19 | 20 | 21 | {fe82bd4a-2d14-486a-8be5-8bf6b50a2414} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | include 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /cpp/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "wrapper.h" 2 | 3 | BOOL APIENTRY DllMain(HMODULE hInstance, DWORD reason, LPVOID lpReserved) 4 | { 5 | switch (reason) { 6 | case DLL_PROCESS_ATTACH: 7 | log_debug("Attaching"); 8 | scriptRegister(hInstance, &Py3kWrapperStart); 9 | keyboardHandlerRegister(&OnKeyboardMessage); 10 | break; 11 | 12 | case DLL_PROCESS_DETACH: 13 | log_debug("Detaching"); 14 | Py3kWrapperStop(); 15 | scriptUnregister(hInstance); 16 | keyboardHandlerUnregister(&OnKeyboardMessage); 17 | break; 18 | } 19 | 20 | return TRUE; 21 | } -------------------------------------------------------------------------------- /cpp/src/natives.i: -------------------------------------------------------------------------------- 1 | %module gta_native 2 | %include "typemaps.i" 3 | 4 | %apply int *OUTPUT { 5 | int* red, int* green, int* blue, 6 | int* tintIndex, 7 | int* r, int* g, int* b, int* a, 8 | int* colorPrimary, int* colorSecondary, 9 | int* pearlescentColor, int* wheelColor, 10 | int* paintType, int* color, 11 | int* ammo, 12 | int* x, int* y, 13 | int* outValue 14 | }; 15 | %apply float *OUTPUT { 16 | float* x, float* y, float* z, float* w, 17 | float* x2d, float* y2d, 18 | float* outValue, 19 | float* height 20 | }; 21 | 22 | %{ 23 | #include "natives_wrap.h" 24 | %} 25 | 26 | %include 27 | %include "../../sdk/inc/natives.h" 28 | %include "../../sdk/inc/enums.h" 29 | %include "../../sdk/inc/types.h" -------------------------------------------------------------------------------- /cpp/src/natives_wrap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "..\..\sdk\inc\natives.h" 4 | #include "..\..\sdk\inc\types.h" 5 | #include "..\..\sdk\inc\enums.h" 6 | 7 | #include "Python.h" 8 | 9 | /* Warning: This is a very nasty hack and has been extracted 10 | from the SWIG generated file. It's likely that this will 11 | fail on other devices. */ 12 | extern "C" { 13 | __declspec(dllexport) 14 | PyObject* PyInit__gta_native(void); 15 | } -------------------------------------------------------------------------------- /cpp/src/wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "wrapper.h" 2 | 3 | Py3kAction action = NONE; 4 | PyThreadState* pThreadState; 5 | PyObject* pExit = nullptr; 6 | PyObject* pTick = nullptr; 7 | PyObject* pKeyEvent = nullptr; 8 | std::ofstream logger("scripthookvpy3k.wrapper.log", std::ios_base::trunc | std::ios_base::out); 9 | 10 | const char* game_version_name(eGameVersion version) { 11 | switch (version) { 12 | case VER_1_0_335_2_STEAM: 13 | return "1.0.335.2_STEAM"; 14 | case VER_1_0_335_2_NOSTEAM: 15 | return "1.0.335.2_NOSTEAM"; 16 | case VER_1_0_350_1_STEAM: 17 | return "1.0.350.1_STEAM"; 18 | case VER_1_0_350_2_NOSTEAM: 19 | return "1.0.350.2_NOSTEAM"; 20 | default: 21 | return "Unknown"; 22 | } 23 | } 24 | 25 | char* wchar_to_string(const wchar_t* wchar_message) { 26 | size_t size = (wcslen(wchar_message) + 1) * 2; 27 | char* message = new char[size]; 28 | size_t converted = 0; 29 | wcstombs_s(&converted, message, size, wchar_message, _TRUNCATE); 30 | return message; 31 | } 32 | 33 | char* time_now() { 34 | char* buffer = new char[80]; 35 | time_t now = time(0); 36 | struct tm time_info; 37 | localtime_s(&time_info, &now); 38 | strftime(buffer, 80, "%Y-%m-%d %X ", &time_info); 39 | return buffer; 40 | } 41 | 42 | void log_(const char* type, const char* message) { 43 | char* now = time_now(); 44 | logger << now << "@" << GetCurrentThreadId() << " " << type << ": " << message << std::endl; 45 | logger.flush(); 46 | delete now; 47 | } 48 | 49 | void log_debug(const char* message) { 50 | log_("Debug", message); 51 | } 52 | 53 | void log_error(const char* message) { 54 | log_("Error", message); 55 | } 56 | 57 | void log_exception(const char* message) { 58 | log_("Exception", message); 59 | } 60 | 61 | std::string Py3kStr(PyObject* obj) { 62 | if (obj != nullptr) { 63 | PyObject* pStr = nullptr; 64 | PyObject* pValue = nullptr; 65 | 66 | // Get representation 67 | pStr = PyObject_Str(obj); 68 | if (pStr == nullptr) { 69 | log_error("Could not retrieve string representation from object"); 70 | return std::string(); 71 | } 72 | 73 | // Convert error value to UTF-8 74 | pValue = PyUnicode_AsUTF8String(pStr); 75 | if (pValue == nullptr) { 76 | log_error("Could not convert Unicode to UTF-8 string"); 77 | Py_DECREF(pStr); 78 | return std::string(); 79 | } 80 | 81 | // Get bytearray as string 82 | char* message = PyBytes_AsString(pValue); 83 | if (message == nullptr) { 84 | log_error("Could not convert bytes to string"); 85 | Py_DECREF(pStr); Py_DECREF(pValue); 86 | return std::string(); 87 | } 88 | 89 | Py_DECREF(pStr); Py_DECREF(pValue); 90 | return std::string(message); 91 | } 92 | else { 93 | return std::string(); 94 | } 95 | } 96 | 97 | bool Py3kException() { 98 | PyObject* pType; 99 | PyObject* pValue; 100 | PyObject* pTraceback; 101 | PyObject* pModule; 102 | PyObject* pList; 103 | PyObject* pEmpty; 104 | PyObject* pMessage; 105 | 106 | // Fetch error (if any) 107 | PyErr_Fetch(&pType, &pValue, &pTraceback); 108 | PyErr_NormalizeException(&pType, &pValue, &pTraceback); 109 | if (pType == nullptr && pValue == nullptr && pTraceback == nullptr) { 110 | // No error 111 | return false; 112 | } 113 | 114 | // Import traceback module 115 | pModule = PyImport_ImportModule("traceback"); 116 | if (pModule == nullptr) { 117 | log_error("Could not import 'traceback' module"); 118 | return true; 119 | } 120 | 121 | // Get formatted exception as an iterable 122 | pList = PyObject_CallMethod( 123 | pModule, "format_exception", "OOO", pType, 124 | pValue == nullptr ? Py_None : pValue, 125 | pTraceback == nullptr ? Py_None : pTraceback 126 | ); 127 | if (pList == nullptr) { 128 | log_error("Invoking traceback.format_exception failed"); 129 | Py_DECREF(pModule); 130 | return true; 131 | } 132 | 133 | // Join iterable with an empty string 134 | pEmpty = PyUnicode_FromString(""); 135 | if (pEmpty == nullptr) { 136 | log_error("Creating empty string failed"); 137 | Py_DECREF(pModule); Py_DECREF(pList); 138 | return true; 139 | } 140 | pMessage = PyObject_CallMethod(pEmpty, "join", "O", pList); 141 | if (pMessage == nullptr) { 142 | log_error("Joining traceback list failed"); 143 | Py_DECREF(pModule); Py_DECREF(pList); Py_DECREF(pEmpty); 144 | return true; 145 | } 146 | 147 | // Log the exception 148 | std::string message = Py3kStr(pMessage); 149 | if (!message.empty()) { 150 | log_exception(message.c_str()); 151 | } 152 | 153 | // Clean up 154 | Py_DECREF(pModule); Py_DECREF(pList); Py_DECREF(pEmpty); Py_DECREF(pMessage); 155 | Py_XDECREF(pType); Py_XDECREF(pValue); Py_XDECREF(pTraceback); 156 | return true; 157 | } 158 | 159 | bool Py3kException(PyObject* obj) { 160 | if (obj == nullptr) { 161 | return Py3kException(); 162 | } else { 163 | return false; 164 | } 165 | } 166 | 167 | void Py3kKeyEvent(int code, bool down, bool alt, bool ctrl, bool shift) { 168 | if (Py_IsInitialized() && pKeyEvent != nullptr) { 169 | PyObject* pArgs; 170 | PyObject* pKwargs; 171 | PyObject* pResult; 172 | 173 | // Acquire GIL 174 | PyEval_RestoreThread(pThreadState); 175 | 176 | // Create arguments 177 | pArgs = Py_BuildValue("iO", 178 | code, // key code 179 | down ? Py_True : Py_False // down 180 | ); 181 | 182 | // Create keyword arguments 183 | pKwargs = Py_BuildValue("{s:O,s:O,s:O}", 184 | "alt", alt ? Py_True : Py_False, 185 | "ctrl", ctrl ? Py_True : Py_False, 186 | "shift", shift ? Py_True : Py_False 187 | ); 188 | 189 | // Pass key event 190 | if (PyCallable_Check(pKeyEvent)) { 191 | pResult = PyObject_Call(pKeyEvent, pArgs, pKwargs); 192 | if (!Py3kException(pResult)) { 193 | Py_DECREF(pResult); 194 | } 195 | } else { 196 | log_error("Key event function is not callable"); 197 | } 198 | 199 | // Clean up 200 | Py_DECREF(pArgs); 201 | Py_DECREF(pKwargs); 202 | 203 | // Release GIL 204 | pThreadState = PyEval_SaveThread(); 205 | } 206 | } 207 | 208 | void Py3kTick() { 209 | if (Py_IsInitialized() && pTick != nullptr) { 210 | PyObject* pResult; 211 | 212 | // Acquire GIL 213 | PyEval_RestoreThread(pThreadState); 214 | 215 | // Call tick function 216 | if (PyCallable_Check(pTick)) { 217 | pResult = PyObject_CallObject(pTick, NULL); 218 | if (!Py3kException(pResult)) { 219 | Py_DECREF(pResult); 220 | } 221 | } else { 222 | log_error("Tick function is not callable"); 223 | } 224 | 225 | // Release GIL 226 | pThreadState = PyEval_SaveThread(); 227 | } 228 | } 229 | 230 | void Py3kInitialize() { 231 | log_debug("Py3kInitialize called"); 232 | 233 | if (!Py_IsInitialized()) { 234 | PyObject* pModule; 235 | PyObject* pInit; 236 | PyObject* pDict; 237 | PyObject* pResult; 238 | PyObject* pOsModule; 239 | PyObject* pPathModule; 240 | PyObject* pCWD; 241 | PyObject* pAbsPathCWD; 242 | PyObject* pSysPath; 243 | PyObject* pStrPythonPath; 244 | PyObject* pJoinedPath; 245 | 246 | // Add module 247 | log_debug("Creating module _gta_native"); 248 | if (PyImport_AppendInittab("_gta_native", &PyInit__gta_native) == -1) { 249 | log_error("Could not extend built-in modules table"); 250 | return; 251 | } 252 | 253 | // Get version (borrowed) 254 | log_debug((std::string("Python ") + std::string(Py_GetVersion())).c_str()); 255 | 256 | // Get path (borrowed) 257 | const wchar_t* path = Py_GetPath(); 258 | if (path == nullptr) { 259 | log_error("Could not retrieve path"); 260 | return; 261 | } 262 | char* cPath = wchar_to_string(path); 263 | log_debug((std::string("Path: ") + std::string(cPath)).c_str()); 264 | delete cPath; 265 | 266 | // Initialise interpreter 267 | log_debug("Initialising"); 268 | Py_InitializeEx(0); 269 | log_debug("Initialised"); 270 | 271 | // Initialise and acquire GIL 272 | log_debug("Initialising and acquiring GIL"); 273 | PyEval_InitThreads(); 274 | 275 | // Get required modules (os, os.path) 276 | pOsModule = PyImport_ImportModule("os"); 277 | if (Py3kException(pOsModule)) { Py_Finalize(); return; } 278 | pPathModule = PyImport_ImportModule("os.path"); 279 | if (Py3kException(pPathModule)) { Py_DECREF(pOsModule); Py_Finalize(); return; } 280 | 281 | // Get current working directory [os.path.abspath(os.getcwd())] 282 | pCWD = PyObject_CallMethod(pOsModule, "getcwd", nullptr); 283 | if (Py3kException(pCWD)) { Py_DECREF(pOsModule); Py_DECREF(pPathModule); Py_Finalize(); return; } 284 | pAbsPathCWD = PyObject_CallMethod(pPathModule, "abspath", "O", pCWD); 285 | if (Py3kException(pAbsPathCWD)) { Py_DECREF(pOsModule); Py_DECREF(pPathModule); Py_DECREF(pCWD); Py_Finalize(); return; } 286 | Py_DECREF(pCWD); Py_DECREF(pOsModule); 287 | // Get sys path (borrowed) [sys.path] 288 | pSysPath = PySys_GetObject("path"); 289 | if (Py3kException(pAbsPathCWD)) { Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_Finalize(); return; } 290 | // Check if working directory is in sys.path 291 | int contains = PySequence_Contains(pSysPath, pAbsPathCWD); 292 | if (contains == -1 || Py3kException()) { Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_Finalize(); return; } 293 | if (contains == 1) { 294 | // Remove from sys path 295 | pResult = PyObject_CallMethod(pSysPath, "remove", "O", pAbsPathCWD); 296 | if (Py3kException(pResult)) { Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_Finalize(); return; } 297 | Py_DECREF(pResult); 298 | } 299 | 300 | // Append modified path 301 | pStrPythonPath = PyUnicode_FromString("python"); 302 | if (Py3kException(pStrPythonPath)) { Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_Finalize(); return; } 303 | pJoinedPath = PyObject_CallMethod(pPathModule, "join", "OO", pAbsPathCWD, pStrPythonPath); 304 | if (Py3kException(pJoinedPath)) { Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_DECREF(pStrPythonPath); Py_Finalize(); return; } 305 | Py_DECREF(pPathModule); Py_DECREF(pAbsPathCWD); Py_DECREF(pStrPythonPath); 306 | pResult = PyObject_CallMethod(pSysPath, "append", "O", pJoinedPath); 307 | if (Py3kException(pResult)) { Py_DECREF(pJoinedPath); Py_Finalize(); return; } 308 | Py_DECREF(pJoinedPath); 309 | Py_DECREF(pResult); 310 | 311 | // Reference module object 312 | log_debug("Importing module: gta"); 313 | pModule = PyImport_ImportModule("gta"); 314 | if (Py3kException(pModule)) { Py_Finalize(); return; } 315 | // Get dictionary of module (borrowed) 316 | pDict = PyModule_GetDict(pModule); 317 | Py_DECREF(pModule); 318 | if (Py3kException(pDict)) { Py_Finalize(); return; } 319 | // Get necessary functions (borrowed) 320 | log_debug("Referencing functions"); 321 | pInit = PyDict_GetItemString(pDict, "_init"); 322 | if (pInit == nullptr) { log_error("'gta._init' does not exist"); Py_Finalize(); return; } 323 | pExit = PyDict_GetItemString(pDict, "_exit"); 324 | if (pExit == nullptr) { log_error("'gta._exit' does not exist"); Py_Finalize(); return; } 325 | pTick = PyDict_GetItemString(pDict, "_tick"); 326 | if (pTick == nullptr) { log_error("'gta._tick' does not exist"); Py_Finalize(); return; } 327 | pKeyEvent = PyDict_GetItemString(pDict, "_key"); 328 | if (pKeyEvent == nullptr) { log_error("'gta._key' does not exist"); Py_Finalize(); return; } 329 | 330 | // Call init function 331 | if (PyCallable_Check(pInit)) { 332 | log_debug("Calling gta._init()"); 333 | pResult = PyObject_CallObject(pInit, NULL); 334 | if (Py3kException(pResult)) { Py_Finalize(); return; } 335 | Py_DECREF(pResult); 336 | log_debug("Returned from gta._init()"); 337 | } else { 338 | log_error("Init function is not callable"); 339 | Py_Finalize(); 340 | return; 341 | } 342 | 343 | // Release GIL 344 | log_debug("Releasing GIL"); 345 | pThreadState = PyEval_SaveThread(); 346 | } else { 347 | log_debug("Already initialised"); 348 | } 349 | } 350 | 351 | void Py3kFinalize() { 352 | log_debug("Py3kFinalize called"); 353 | 354 | if (Py_IsInitialized()) { 355 | PyObject* pResult; 356 | 357 | log_debug("Finalising"); 358 | 359 | // Acquire GIL 360 | log_debug("Acquiring GIL"); 361 | PyEval_RestoreThread(pThreadState); 362 | 363 | // Call exit function 364 | if (PyCallable_Check(pExit)) { 365 | log_debug("Calling gta._exit()"); 366 | pResult = PyObject_CallObject(pExit, NULL); 367 | if (!Py3kException(pResult)) { 368 | log_debug("Returned from gta._exit()"); 369 | Py_DECREF(pResult); 370 | } 371 | } else { 372 | log_error("Exit function is not callable"); 373 | } 374 | 375 | // Finalise interpreter 376 | log_debug("Finalising"); 377 | Py_Finalize(); 378 | log_debug("Finalised"); 379 | 380 | // Reset vars 381 | pExit = nullptr; 382 | pTick = nullptr; 383 | pKeyEvent = nullptr; 384 | pThreadState = nullptr; 385 | } else { 386 | log_debug("Not initialised"); 387 | } 388 | } 389 | 390 | void Py3kReinitialize() { 391 | Py3kFinalize(); 392 | Py3kInitialize(); 393 | } 394 | 395 | void OnKeyboardMessage(DWORD key, WORD repeats, BYTE scanCode, BOOL isExtended, BOOL isWithAlt, BOOL wasDownBefore, BOOL isUpNow) { 396 | int code = static_cast(key); 397 | bool down = isUpNow == FALSE; 398 | bool alt = isWithAlt == TRUE; 399 | bool ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; 400 | bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; 401 | 402 | // Catch built-in key events 403 | if (ctrl && !down) { 404 | if (key == VK_DELETE) { 405 | // Stop on Ctrl + Del 406 | action = STOP; 407 | log_debug("Stop action set"); 408 | return; 409 | } else if (key == VK_F12) { 410 | // Reload on Ctrl + F12 411 | action = RESTART; 412 | log_debug("Restart action set"); 413 | return; 414 | } 415 | } 416 | 417 | // Propagate key event 418 | Py3kKeyEvent(code, down, alt, ctrl, shift); 419 | } 420 | 421 | void Py3kWrapperStart() { 422 | log_debug("Py3kWrapper called"); 423 | log_debug((std::string("Version: ") + std::string(PY3KWRAPPER_VERSION)).c_str()); 424 | log_debug((std::string("Game Version: ") + std::string(game_version_name(getGameVersion()))).c_str()); 425 | 426 | // (Re)Initialise 427 | Py3kReinitialize(); 428 | 429 | // Main loop 430 | log_debug("Entering main loop"); 431 | while (true) { 432 | switch (action) { 433 | case STOP: 434 | // Stop 435 | log_debug("Enforcing stop"); 436 | Py3kFinalize(); 437 | action = NONE; 438 | break; 439 | case RESTART: 440 | // Restart 441 | log_debug("Reloading"); 442 | Py3kReinitialize(); 443 | action = NONE; 444 | break; 445 | default: 446 | // Tick 447 | Py3kTick(); 448 | } 449 | 450 | // Yield 451 | scriptWait(0); 452 | } 453 | log_error("Exited main loop"); 454 | } 455 | 456 | void Py3kWrapperStop() { 457 | // Finalise 458 | Py3kFinalize(); 459 | } 460 | 461 | 462 | -------------------------------------------------------------------------------- /cpp/src/wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "..\..\sdk\inc\natives.h" 4 | #include "..\..\sdk\inc\types.h" 5 | #include "..\..\sdk\inc\enums.h" 6 | 7 | #include "..\..\sdk\inc\main.h" 8 | 9 | #include "natives_wrap.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #define PY3KWRAPPER_VERSION "0.9.9" 16 | 17 | enum Py3kAction : int { 18 | NONE, 19 | STOP, 20 | RESTART 21 | }; 22 | 23 | const char* game_version_name(eGameVersion version); 24 | char* wchar_to_string(const wchar_t* wchar_message); 25 | char* time_now(); 26 | void log_(const char* type, const char* message); 27 | void log_debug(const char* message); 28 | void log_error(const char* message); 29 | void log_exception(const char* message); 30 | std::string Py3kStr(PyObject* obj); 31 | bool Py3kException(); 32 | bool Py3kException(PyObject* obj); 33 | void Py3kKeyEvent(int code, bool down, bool alt, bool ctrl, bool shift); 34 | void Py3kTick(); 35 | void Py3kInitialize(); 36 | void Py3kFinalize(); 37 | void Py3kReinitialize(); 38 | void OnKeyboardMessage(DWORD key, WORD repeats, BYTE scanCode, BOOL isExtended, BOOL isWithAlt, BOOL wasDownBefore, BOOL isUpNow); 39 | void Py3kWrapperStart(); 40 | void Py3kWrapperStop(); 41 | -------------------------------------------------------------------------------- /python/gta/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Enable asyncio debug logging 4 | os.environ['PYTHONASYNCIODEBUG'] = '1' 5 | 6 | import ast 7 | import functools 8 | import pkgutil 9 | import importlib 10 | import asyncio 11 | import threading 12 | import atexit 13 | 14 | import gta_native 15 | 16 | from gta import ui, exceptions, enums 17 | from gta.exceptions import * 18 | from gta.enums import * 19 | 20 | __author__ = 'Lennart Grahl ' 21 | __status__ = 'Development' 22 | __version__ = '0.10.16' 23 | __all__ = exceptions.__all__ + enums.__all__ 24 | 25 | 26 | def _reset_globals(): 27 | """ 28 | Set global attributes. 29 | """ 30 | global _utils, _thread, _loop, _tasks, _names 31 | _utils = None 32 | _thread = None 33 | _loop = None 34 | _tasks = [] 35 | _names = [] 36 | 37 | 38 | def _reset_futures(loop): 39 | """ 40 | Set global futures. 41 | 42 | Arguments: 43 | - `loop`: The loop the futures will be assigned to. 44 | """ 45 | global _tick_future, _key_future 46 | try: 47 | _tick_future.cancel() 48 | _key_future.cancel() 49 | except NameError: 50 | pass 51 | _tick_future = asyncio.Future(loop=loop) 52 | _key_future = asyncio.Future(loop=loop) 53 | 54 | 55 | def _reset_viewport(): 56 | """ 57 | Set global UI view port. 58 | """ 59 | ui.reset() 60 | 61 | 62 | def _init(console=False): 63 | """ 64 | Run startup function in another thread. 65 | 66 | Arguments: 67 | - `console`: Use console logging instead of file logging. 68 | """ 69 | _reset_globals() 70 | global _thread 71 | 72 | # Start thread 73 | _thread = threading.Thread(target=_start, args=(console,), daemon=True) 74 | _thread.start() 75 | 76 | 77 | def _start(console): 78 | """ 79 | Initialise requirements and startup scripts in an event loop. 80 | 81 | Arguments: 82 | - `console`: Use console logging instead of file logging. 83 | """ 84 | global _utils, _loop, _names, _tasks 85 | 86 | # Import utils 87 | # Note: This needs to be done here because the logging module binds 88 | # some vars to the thread which causes exceptions when 89 | # pip invokes the logger. 90 | from gta import utils 91 | _utils = utils 92 | 93 | # Setup logging 94 | _utils.setup_logging(console) 95 | logger = _utils.get_logger() 96 | 97 | # Create event loop 98 | _loop = asyncio.new_event_loop() 99 | asyncio.set_event_loop(_loop) 100 | 101 | # Reset futures and viewport 102 | _reset_futures(_loop) 103 | _reset_viewport() 104 | 105 | # Print some debug information 106 | logger.info('Started') 107 | logger.info('Version: {}', __version__) 108 | logger.info('Natives Date: {}', gta_native.__version__) 109 | 110 | # Start scripts 111 | _names, _tasks = _start_scripts(_loop) 112 | if len(_tasks) > 0: 113 | try: 114 | _loop.run_until_complete(asyncio.wait(_tasks)) 115 | except RuntimeError: 116 | bad_scripts = [(name, task) for name, task in zip(_names, _tasks) 117 | if not task.done()] 118 | 119 | # Mark bad behaving scripts as done 120 | for name, task in bad_scripts: 121 | # Note: We log the task so scripters can see in which line their script 122 | # was running when cancelled 123 | logger.error('Script "{}" did not stop in time, Task: {}', name, task) 124 | # Note: At this point, the task is marked as done but callbacks will 125 | # not be called anymore. We are just doing this to comfort asyncio 126 | # to not throw any exceptions because the task wasn't marked done 127 | task.set_result(BadBehavingScriptError()) 128 | 129 | # Report bad behaving scripts 130 | scripts = ', '.join(('"{}"'.format(name) for name, task in bad_scripts)) 131 | logger.warning('Enforced stopping loop, caused by script(s): {}', scripts) 132 | 133 | logger.info('Complete') 134 | 135 | 136 | def _tick(): 137 | """ 138 | Handle a game tick event. 139 | """ 140 | if _loop is not None and not _loop.is_closed(): 141 | def __tick(): 142 | global _tick_future 143 | _tick_future.set_result(None) 144 | _tick_future = asyncio.Future(loop=_loop) 145 | #ui.draw() 146 | _loop.call_soon_threadsafe(__tick) 147 | 148 | 149 | def _key(code, down, **modifiers): 150 | """ 151 | Handle a key event. 152 | 153 | Arguments: 154 | - `code`: The key code represented as an integer. 155 | - `down`: `True` if the key is pressed, `False` if the key was just released. 156 | - `modifiers`: Modifier keys pressed. 157 | """ 158 | if _loop is not None and not _loop.is_closed(): 159 | # Translate key code 160 | code = Key(code) 161 | # logger = _utils.get_logger() 162 | # logger.debug("Key '{}', down: {}, modifiers: {}", code, down, modifiers) 163 | 164 | def __key(): 165 | global _key_future 166 | _key_future.set_result((code, down, modifiers)) 167 | _key_future = asyncio.Future(loop=_loop) 168 | _loop.call_soon_threadsafe(__key) 169 | 170 | 171 | def _exit(): 172 | """ 173 | Schedule stopping scripts. 174 | """ 175 | if _loop is not None and not _loop.is_closed(): 176 | logger = _utils.get_logger() 177 | logger.debug('Scheduling script termination') 178 | 179 | # Schedule stop routine 180 | def __stop(loop): 181 | logger.debug('Stopping scripts') 182 | loop.create_task(_stop(loop)) 183 | _loop.call_soon_threadsafe(__stop, _loop) 184 | 185 | 186 | @atexit.register 187 | def _join(): 188 | """ 189 | Try to join the event loop thread. 190 | """ 191 | # Note: _utils might be none when _init wasn't called 192 | if _utils is None: 193 | return 194 | logger = _utils.get_logger() 195 | 196 | # Wait until the thread of the event loop terminates 197 | if _thread is not None: 198 | logger.debug('Joining') 199 | _thread.join(timeout=1.1) 200 | if _thread.is_alive(): 201 | logger.error('Joining timed out, terminating ungracefully') 202 | 203 | # Reset globals and exit 204 | _reset_globals() 205 | logger.info('Exiting') 206 | 207 | 208 | @asyncio.coroutine 209 | def _stop(loop, seconds=1.0): 210 | """ 211 | Stop scripts, wait for tasks to clean up or until a timeout occurs 212 | and stop the loop. 213 | 214 | Arguments: 215 | - `loop`: The :class:`asyncio.BaseEventLoop` that is being used. 216 | - `seconds`: The maximum amount of seconds to wait. 217 | """ 218 | logger = _utils.get_logger() 219 | 220 | # Stop scripts 221 | _stop_scripts(_tasks) 222 | 223 | # Wait for scripts to clean up 224 | logger.debug('Waiting for scripts to stop') 225 | yield from asyncio.wait(_tasks, timeout=seconds) 226 | 227 | # Stop loop 228 | logger.debug('Stopping loop') 229 | loop.stop() 230 | 231 | 232 | def _start_scripts(loop): 233 | """ 234 | Run the main function of all scripts from the `scripts` package. 235 | 236 | Arguments: 237 | - `loop`: The :class:`asyncio.BaseEventLoop` that is going to be used. 238 | 239 | Return a tuple containing a list of imported script names and 240 | another list that maps the script names to:class:`asyncio.Task` 241 | instances. 242 | """ 243 | logger = _utils.get_logger() 244 | logger.info('Starting scripts') 245 | 246 | # Start each script as a coroutine 247 | names = [] 248 | tasks = [] 249 | for name, script in _import_scripts(): 250 | logger.info('Starting script "{}"', name) 251 | task = loop.create_task(script()) 252 | task.add_done_callback(functools.partial(_script_done, name=name)) 253 | names.append(name) 254 | tasks.append(task) 255 | logger.info('Scripts started') 256 | return names, tasks 257 | 258 | 259 | def _stop_scripts(tasks): 260 | """ 261 | Cancel scripts that are still running. 262 | 263 | Arguments: 264 | - `tasks`: A list of :class:`asyncio.Task` instances. 265 | """ 266 | logger = _utils.get_logger() 267 | logger.info('Cancelling scripts') 268 | for task in tasks: 269 | task.cancel() 270 | logger.info('Scripts cancelled') 271 | 272 | 273 | def _import_scripts(): 274 | """ 275 | Import all scripts from the `scripts` package and install 276 | dependencies. 277 | 278 | Return a list containing tuples of each scripts name and the 279 | callback to the main function of the script. 280 | """ 281 | logger = _utils.get_logger() 282 | 283 | # Import parent package 284 | parent_package = 'scripts' 285 | importlib.import_module(parent_package, __name__) 286 | 287 | # Import scripts from package 288 | path = os.path.join(_utils.get_directory(), parent_package) 289 | scripts = [] 290 | for importer, name, is_package in pkgutil.iter_modules([path]): 291 | try: 292 | try: 293 | # Get meta data 294 | metadata = _scrape_metadata(path, name, is_package) 295 | logger.debug('Script "{}" metadata: {}', name, metadata) 296 | # Get dependencies from meta data 297 | dependencies = metadata.get('dependencies', ()) 298 | # Make to tuple if string 299 | if isinstance(dependencies, str): 300 | dependencies = (dependencies,) 301 | except AttributeError: 302 | dependencies = () 303 | 304 | try: 305 | # Install dependencies 306 | for dependency in dependencies: 307 | _utils.install_dependency(dependency) 308 | except TypeError as exc: 309 | raise ScriptError() from exc 310 | 311 | try: 312 | # Import script 313 | logger.debug('Importing script "{}"', name) 314 | module = importlib.import_module('.' + name, parent_package) 315 | main = getattr(module, 'main') 316 | # Make sure that main is a co-routine 317 | if not asyncio.iscoroutinefunction(main): 318 | raise ScriptError( 319 | 'Main function of script "{}" is not a co-routine'.format(name)) 320 | scripts.append((name, main)) 321 | except (ImportError, AttributeError) as exc: 322 | raise ImportScriptError(name) from exc 323 | except ScriptError as exc: 324 | # Note: We are not re-raising here because script errors should not 325 | # affect other scripts that run fine 326 | logger.exception(exc) 327 | 328 | # Return scripts list 329 | return scripts 330 | 331 | 332 | def _scrape_metadata(path, name, is_package): 333 | # Update path 334 | if is_package: 335 | path = os.path.join(path, name, '__init__.py') 336 | else: 337 | path = os.path.join(path, name + '.py') 338 | 339 | # Open script path 340 | metadata = {} 341 | with open(path) as file: 342 | for line in file: 343 | # Find metadata strings 344 | if line.startswith('__'): 345 | try: 346 | # Store key and value 347 | key, value = line.split('=', maxsplit=1) 348 | key = key.strip().strip('__') 349 | # Note: Literal eval tries to retrieve a value, assignments, 350 | # calls, etc. are not possible 351 | value = ast.literal_eval(value.strip()) 352 | metadata[key] = value 353 | except (ValueError, SyntaxError) as exc: 354 | raise ImportScriptError(name) from exc 355 | return metadata 356 | 357 | 358 | def _script_done(task, name=None): 359 | """ 360 | Log the result or the exception of a script that returned. 361 | 362 | Arguments: 363 | - `task`: The :class:`asyncio.Future` instance of the script. 364 | - `name`: The name of the script. 365 | """ 366 | logger = _utils.get_logger() 367 | 368 | try: 369 | try: 370 | # Check for exception or result 371 | script_exc = task.exception() 372 | if script_exc is not None: 373 | raise ScriptExecutionError(name) from script_exc 374 | else: 375 | result = task.result() 376 | result = ' with result "{}"'.format(result) if result is not None else '' 377 | logger.info('Script "{}" returned{}', name, result) 378 | except asyncio.CancelledError: 379 | logger.info('Script "{}" cancelled', name) 380 | except asyncio.InvalidStateError as exc: 381 | raise ScriptError('Script "{}" done callback called but script is not done' 382 | ''.format(name)) from exc 383 | except ScriptError as exc: 384 | logger.exception(exc) 385 | -------------------------------------------------------------------------------- /python/gta/enums.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various enumerators for GTA V and events. 3 | """ 4 | import enum 5 | 6 | __all__ = ('Key', 'Font') 7 | 8 | 9 | @enum.unique 10 | class Key(enum.Enum): 11 | """ 12 | Map virtual-key codes to names. 13 | 14 | See `MSDN Virtual-Key Codes `_ 15 | for further information. 16 | """ 17 | LBUTTON = 1 # Left mouse button 18 | RBUTTON = 2 # Right mouse button 19 | CANCEL = 3 # Control-break processing 20 | MBUTTON = 4 # Middle mouse button (three-button mouse) 21 | BACK = 8 # BACKSPACE key 22 | TAB = 9 # TAB key 23 | CLEAR = 12 # CLEAR key 24 | RETURN = 13 # ENTER key 25 | SHIFT = 16 # SHIFT key 26 | CONTROL = 17 # CTRL key 27 | MENU = 18 # ALT key 28 | PAUSE = 19 # PAUSE key 29 | CAPITAL = 20 # CAPS LOCK key 30 | ESCAPE = 27 # ESC key 31 | SPACE = 32 # SPACEBAR 32 | PRIOR = 33 # PAGE UP key 33 | NEXT = 34 # PAGE DOWN key 34 | END = 35 # END key 35 | HOME = 36 # HOME key 36 | LEFT = 37 # LEFT ARROW key 37 | UP = 38 # UP ARROW key 38 | RIGHT = 39 # RIGHT ARROW key 39 | DOWN = 40 # DOWN ARROW key 40 | SELECT = 41 # SELECT key 41 | PRINT = 42 # PRINT key 42 | EXECUTE = 43 # EXECUTE key 43 | SNAPSHOT = 44 # PRINT SCREEN key 44 | INSERT = 45 # INS key 45 | DELETE = 46 # DEL key 46 | HELP = 47 # HELP key 47 | N0 = 48 # 0 key 48 | N1 = 49 # 1 key 49 | N2 = 50 # 2 key 50 | N3 = 51 # 3 key 51 | N4 = 52 # 4 key 52 | N5 = 53 # 5 key 53 | N6 = 54 # 6 key 54 | N7 = 55 # 7 key 55 | N8 = 56 # 8 key 56 | N9 = 57 # 9 key 57 | A = 65 58 | B = 66 59 | C = 67 60 | D = 68 61 | E = 69 62 | F = 70 63 | G = 71 64 | H = 72 65 | I = 73 66 | J = 74 67 | K = 75 68 | L = 76 69 | M = 77 70 | N = 78 71 | O = 79 72 | P = 80 73 | Q = 81 74 | R = 82 75 | S = 83 76 | T = 84 77 | U = 85 78 | V = 86 79 | W = 87 80 | X = 88 81 | Y = 89 82 | Z = 90 83 | LWIN = 91 # Left Windows key (Natural keyboard) 84 | RWIN = 92 # Right Windows key (Natural keyboard) 85 | APPS = 93 # Applications key (Natural keyboard) 86 | NUMPAD0 = 96 # Numeric keypad 0 key 87 | NUMPAD1 = 97 # Numeric keypad 1 key 88 | NUMPAD2 = 98 # Numeric keypad 2 key 89 | NUMPAD3 = 99 # Numeric keypad 3 key 90 | NUMPAD4 = 100 # Numeric keypad 4 key 91 | NUMPAD5 = 101 # Numeric keypad 5 key 92 | NUMPAD6 = 102 # Numeric keypad 6 key 93 | NUMPAD7 = 103 # Numeric keypad 7 key 94 | NUMPAD8 = 104 # Numeric keypad 8 key 95 | NUMPAD9 = 105 # Numeric keypad 9 key 96 | MULTIPLY = 106 # Multiply key 97 | ADD = 107 # Add key 98 | SEPARATOR = 108 # Separator key 99 | SUBTRACT = 109 # Subtract key 100 | DECIMAL = 110 # Decimal key 101 | DIVIDE = 111 # Divide key 102 | F1 = 112 103 | F2 = 113 104 | F3 = 114 105 | F4 = 115 106 | F5 = 116 107 | F6 = 117 108 | F7 = 118 109 | F8 = 119 110 | F9 = 120 111 | F10 = 121 112 | F11 = 122 113 | F12 = 123 114 | F13 = 124 115 | F14 = 125 116 | F15 = 126 117 | F16 = 127 118 | F17 = 128 119 | F18 = 129 120 | F19 = 130 121 | F20 = 131 122 | F21 = 132 123 | F22 = 133 124 | F23 = 134 125 | F24 = 135 126 | NUMLOCK = 144 # NUM LOCK key 127 | SCROLL = 145 # SCROLL LOCK key 128 | LSHIFT = 160 # Left SHIFT key 129 | RSHIFT = 161 # Right SHIFT key 130 | LCONTROL = 162 # Left CONTROL key 131 | RCONTROL = 163 # Right CONTROL key 132 | LMENU = 164 # Left MENU key 133 | RMENU = 165 # Right MENU key 134 | OEM_1 = 186 # May vary, US: ';:' key 135 | OEM_PLUS = 187 # '+' key 136 | OEM_COMMA = 188 # ',' key 137 | OEM_MINUS = 189 # '-' key 138 | OEM_PERIOD = 190 # '.' key 139 | OEM_2 = 191 # May vary, US: '/?' key 140 | OEM_3 = 192 # May vary, US: '~' key 141 | OEM_4 = 219 # May vary, US: '[{' key 142 | OEM_5 = 220 # May vary, US: '\|' key 143 | OEM_6 = 221 # May vary, US: ']}' key 144 | OEM_7 = 222 # May vary, US: single-quote/double-quote key 145 | 146 | 147 | @enum.unique 148 | class Font(enum.IntEnum): 149 | chalet_london = 0 150 | house_script = 1 151 | monospace = 2 152 | chalet_comprime_cologne = 4 153 | pricedown = 7 154 | -------------------------------------------------------------------------------- /python/gta/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Commonly used events that can be waited for. 3 | 4 | Every function returns a coroutine that has to be yielded from. 5 | """ 6 | import asyncio 7 | import functools 8 | import gta 9 | 10 | from gta import utils 11 | 12 | __all__ = ('tick', 'key', 'wait') 13 | 14 | 15 | @asyncio.coroutine 16 | def tick(count=1): 17 | """ 18 | Wait for one or more game ticks. 19 | 20 | .. warning:: Instead of using this function in your script, you 21 | should write a coroutine in the `requires` package 22 | and create a pull request on GitHub. 23 | 24 | Arguments: 25 | - `count`: The amount of game ticks to wait for. 26 | """ 27 | ticks = 0 28 | while count > ticks: 29 | yield from asyncio.shield(getattr(gta, '_tick_future')) 30 | ticks += 1 31 | return 32 | 33 | 34 | @asyncio.coroutine 35 | def key(codes=None, down=False, **modifiers): 36 | """ 37 | Wait for a key to be pressed or released. 38 | 39 | Arguments: 40 | - `code`: A single :class:`Key` or a list of :class:`Key`s to 41 | watch for. Use ``None`` to watch all keys. 42 | - `down`: ``True`` returns keys when pressed, ``False`` returns 43 | keys when released. Use ``None`` for both cases. 44 | - `alt`: ``True`` requires `Alt` to be pressed as well, 45 | ``False`` requires `Alt` to be not pressed. Defaults to both 46 | cases. 47 | - `ctrl`: ``True`` requires `Ctrl` to be pressed as well, 48 | ``False`` requires `Ctrl` to be not pressed. Defaults to both 49 | cases. 50 | - `shift`: ``True`` requires `Shift` to be pressed as well, 51 | ``False`` requires `Shift` to be not pressed. Defaults to both 52 | cases. 53 | 54 | Return a tuple containing the actual key event values for `code`, 55 | `down` and `modifiers`. 56 | """ 57 | if isinstance(codes, gta.Key): 58 | # Convert codes to tuple 59 | codes = (codes,) 60 | elif codes is not None and not isinstance(codes, set): 61 | # Convert iterable to set 62 | codes = set(codes) 63 | 64 | while True: 65 | # Unpack key event 66 | key_event = yield from asyncio.shield(getattr(gta, '_key_future')) 67 | code, down_, modifiers_ = key_event 68 | 69 | # Check code 70 | if codes is not None and code not in codes: 71 | continue 72 | # Check down 73 | if down is not None and down != down_: 74 | continue 75 | # Check modifiers 76 | if any((modifiers_[key_] != value for key_, value in modifiers.items())): 77 | continue 78 | 79 | # Return key event 80 | return key_event 81 | 82 | 83 | @asyncio.coroutine 84 | def wait(require_func, *args, precision=10, **kwargs): 85 | """ 86 | Wait for a requirement to be fulfilled. 87 | 88 | Arguments: 89 | - `require_func`: A function from the :mod:`requires` package. 90 | - `precision`: The amount of game ticks to wait between checks. 91 | - `args`: Arguments that will be passed to the function. 92 | - `kwargs`: Keyword arguments that will be passed to the 93 | function. 94 | 95 | Return the value `require_func` returns when the requirement is 96 | fulfilled. 97 | """ 98 | logger = utils.get_logger('gta.wait') 99 | 100 | # Create partial 101 | partial = functools.partial(require_func, *args, **kwargs) 102 | 103 | # Poll until fulfilled 104 | while True: 105 | try: 106 | result = partial() 107 | logger.debug('{} fulfilled, result: {}', partial, result) 108 | return result 109 | except gta.RequirementError as exc: 110 | logger.debug('{} not fulfilled, missing: {}', partial, exc.requirement) 111 | yield from tick(count=precision) 112 | -------------------------------------------------------------------------------- /python/gta/exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ('ScriptError', 'ImportScriptError', 'InstallDependencyError', 2 | 'DependencyBlacklistedError', 'ScriptExecutionError', 'BadBehavingScriptError', 3 | 'RequirementError') 4 | 5 | 6 | class ScriptError(Exception): 7 | """ 8 | A general script exception all other exceptions are derived from. 9 | """ 10 | pass 11 | 12 | 13 | class ImportScriptError(ScriptError): 14 | """ 15 | A script could not be imported. 16 | 17 | Arguments: 18 | - `name`: The name of the script. 19 | """ 20 | def __init__(self, name): 21 | self.name = name 22 | 23 | def __str__(self): 24 | return 'Could not import script "{}"'.format(self.name) 25 | 26 | 27 | class InstallDependencyError(ScriptError): 28 | """ 29 | A script dependency could not be installed. 30 | 31 | Arguments: 32 | - `dependency`: The dependency name. 33 | """ 34 | def __init__(self, dependency): 35 | self.dependency = dependency 36 | 37 | def __str__(self): 38 | return 'Dependency needs to be installed manually via pip "{}"'.format( 39 | self.dependency) 40 | 41 | 42 | class DependencyBlacklistedError(ScriptError): 43 | """ 44 | A script dependency is blacklisted and cannot be installed. 45 | 46 | Arguments: 47 | - `dependency`: The dependency name. 48 | """ 49 | def __init__(self, dependency): 50 | self.dependency = dependency 51 | 52 | def __str__(self): 53 | return 'Could not install dependency "{}"'.format(self.dependency) 54 | 55 | 56 | class ScriptExecutionError(ScriptError): 57 | """ 58 | An uncaught exception was raised in a script. 59 | 60 | Arguments: 61 | - `name`: The name of the script. 62 | """ 63 | def __init__(self, name): 64 | self.name = name 65 | 66 | def __str__(self): 67 | return 'Script "{}" returned with an exception'.format(self.name) 68 | 69 | 70 | class BadBehavingScriptError(ScriptError): 71 | """ 72 | A script did not stop in time after it has been cancelled. 73 | """ 74 | 75 | 76 | class RequirementError(ScriptError): 77 | """ 78 | A general requirement exception. 79 | 80 | Arguments: 81 | - `requirement`: One or more requirements that are missing. 82 | """ 83 | def __init__(self, requirement): 84 | self.requirement = requirement 85 | 86 | def __str__(self): 87 | return 'Missing: {}'.format(self.requirement) 88 | -------------------------------------------------------------------------------- /python/gta/requires/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Commonly used functions that have one or more requirements. 3 | 4 | Every function returns either a result or raises a 5 | :class:`RequirementError`, so multiple functions can easily be chained 6 | without having to validate the result after each call. 7 | """ 8 | -------------------------------------------------------------------------------- /python/gta/requires/ped.py: -------------------------------------------------------------------------------- 1 | import gta_native 2 | 3 | from gta import exceptions 4 | 5 | __all__ = ('get_vehicle',) 6 | 7 | 8 | def get_vehicle(ped): 9 | """ 10 | Return the vehicle a ped is using. 11 | 12 | Arguments: 13 | - `ped`: The id of the ped. 14 | """ 15 | # Check if the ped is in a vehicle 16 | if gta_native.ped.is_ped_in_any_vehicle(ped, 0): 17 | return gta_native.ped.get_vehicle_ped_is_using(ped) 18 | else: 19 | raise exceptions.RequirementError('Vehicle of ped {}'.format(ped)) 20 | -------------------------------------------------------------------------------- /python/gta/requires/player.py: -------------------------------------------------------------------------------- 1 | import gta_native 2 | 3 | from gta import exceptions 4 | from gta.requires import ped 5 | 6 | __all__ = ('get_id', 'get_ped', 'get_vehicle') 7 | 8 | def get_id(check_ped=True): 9 | """ 10 | Return the player id. 11 | 12 | Arguments: 13 | - `check_ped`: If ``True``, make sure that the player's 14 | ped entity exists. 15 | """ 16 | if check_ped: 17 | get_ped() 18 | return gta_native.player.player_id() 19 | 20 | 21 | def get_ped(): 22 | """ 23 | Return the player ped. 24 | """ 25 | # Check if the player ped and entity exists 26 | player_ped = gta_native.player.player_ped_id() 27 | if gta_native.entity.does_entity_exist(player_ped): 28 | return player_ped 29 | else: 30 | raise exceptions.RequirementError('Player entity') 31 | 32 | 33 | def get_vehicle(): 34 | """ 35 | Return the vehicle the player is using. 36 | """ 37 | return ped.get_vehicle(get_ped()) 38 | -------------------------------------------------------------------------------- /python/gta/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | UI components that can be used in GTA V. 3 | """ 4 | import enum 5 | import collections 6 | import weakref 7 | 8 | __all__ = ('Point', 'Color', 'Direction', 'Align', 9 | 'Item', 'SelectableItem', 'ActivatableItem', 'AlterableItem', 'Container', 10 | 'add', 'remove') 11 | 12 | 13 | class Point(collections.namedtuple('Point', 'x, y')): 14 | """ 15 | A 2D point containing `x`- and `y`-coordinates. 16 | 17 | Arguments: 18 | - `x`: A `x`-coordinate value between ``0`` and ``1``. 19 | - `y`: A `y`-coordinate value between ``0`` and ``1``. 20 | """ 21 | Zero = TopLeft = TopRight = BottomLeft = BottomRight = Center = None 22 | __slots__ = () 23 | 24 | # Point shorthands 25 | Point.Zero = Point(0.0, 0.0) 26 | Point.TopLeft = Point(0.0, 0.0) 27 | Point.TopRight = Point(1.0, 0.0) 28 | Point.BottomLeft = Point(0.0, 1.0) 29 | Point.BottomRight = Point(1.0, 1.0) 30 | Point.Center = Point(0.5, 0.5) 31 | 32 | 33 | class Dimension(collections.namedtuple('Dimension', 'width, height')): 34 | """ 35 | A 2D dimension containing width and height. 36 | 37 | Arguments: 38 | - `width`: A width value between ``0`` and ``1``. 39 | - `height`: A height value between ``0`` and ``1``. 40 | """ 41 | Zero = Quarter = Half = Full = None 42 | __slots__ = () 43 | 44 | # Dimension shorthands 45 | Dimension.Zero = Dimension(0.0, 0.0) 46 | Dimension.Quarter = Dimension(0.25, 0.25) 47 | Dimension.Half = Dimension(0.5, 0.5) 48 | Dimension.Full = Dimension(1.0, 1.0) 49 | 50 | 51 | class Distance(collections.namedtuple('Distance', 'top, right, bottom, left')): 52 | """ 53 | Represents the space around an UI element. 54 | 55 | Arguments: 56 | - `top`: Space around the top. 57 | - `right`: Space around the right. 58 | - `bottom`: Space around the bottom. 59 | - `left:`: Space around the left. 60 | 61 | Alternatively, a single CSS-like margin string can be supplied 62 | with support for one to four values. 63 | """ 64 | Zero = None 65 | __slots__ = () 66 | 67 | def __new__(cls, *args, **kwargs): 68 | # Convert CSS-like margin string 69 | distance, *_ = args 70 | if isinstance(distance, str): 71 | values = [float(value) for value in distance.strip().split(' ')] 72 | top, right, bottom, left = [0.0] * 4 73 | length = len(values) 74 | if length == 1: 75 | top = right = bottom = left = values[0] 76 | elif length == 2: 77 | top, right = values 78 | bottom = top 79 | left = right 80 | elif length == 3: 81 | top, right, bottom = values 82 | left = right 83 | elif length == 4: 84 | top, right, bottom, left = values 85 | return super().__new__(cls, top, right, bottom, left) 86 | return super().__new__(cls, *args, **kwargs) 87 | 88 | # Distance shorthands 89 | Distance.Zero = Distance('0') 90 | 91 | 92 | class Color(collections.namedtuple('Color', 'r, g, b, a')): 93 | """ 94 | Represents an RGB (red, green, colour) value with an additional 95 | alpha channel. 96 | 97 | Arguments: 98 | - `r`: A red colour value between ``0`` and ``255``. 99 | - `g`: A green colour value between ``0`` and ``255``. 100 | - `b`: A blue colour value between ``0`` and ``255``. 101 | - `a`: An alpha channel value between ``0`` and ``255``. 102 | Defaults to ``255``. 103 | 104 | Alternatively, a single CSS-like hexadecimal colour string can be 105 | supplied. 106 | """ 107 | Aqua = AliceBlue = AntiqueWhite = Black = Blue = Cyan = DarkBlue = DarkCyan =\ 108 | DarkGreen = DarkTurquoise = DeepSkyBlue = Green = Lime = MediumBlue =\ 109 | MediumSpringGreen = Navy = SpringGreen = Teal = MidnightBlue = DodgerBlue =\ 110 | LightSeaGreen = ForestGreen = SeaGreen = DarkSlateGray = DarkSlateGrey =\ 111 | LimeGreen = MediumSeaGreen = Turquoise = RoyalBlue = SteelBlue = DarkSlateBlue\ 112 | = MediumTurquoise = Indigo = DarkOliveGreen = CadetBlue = CornflowerBlue =\ 113 | MediumAquamarine = DimGray = DimGrey = SlateBlue = OliveDrab = SlateGray =\ 114 | SlateGrey = LightSlateGray = LightSlateGrey = MediumSlateBlue = LawnGreen =\ 115 | Aquamarine = Chartreuse = Gray = Grey = Maroon = Olive = Purple = LightSkyBlue =\ 116 | SkyBlue = BlueViolet = DarkMagenta = DarkRed = SaddleBrown = DarkSeaGreen =\ 117 | LightGreen = MediumPurple = DarkViolet = PaleGreen = DarkOrchid = YellowGreen\ 118 | = Sienna = Brown = DarkGray = DarkGrey = GreenYellow = LightBlue = PaleTurquoise\ 119 | = LightSteelBlue = PowderBlue = FireBrick = DarkGoldenrod = MediumOrchid =\ 120 | RosyBrown = DarkKhaki = Silver = MediumVioletRed = IndianRed = Peru = Chocolate\ 121 | = Tan = LightGray = LightGrey = Thistle = Goldenrod = Orchid = PaleVioletRed =\ 122 | Crimson = Gainsboro = Plum = BurlyWood = LightCyan = Lavender = DarkSalmon =\ 123 | PaleGoldenrod = Violet = Azure = Honeydew = Khaki = LightCoral = SandyBrown =\ 124 | Beige = MintCream = Wheat = WhiteSmoke = GhostWhite = LightGoldenrodYellow =\ 125 | Linen = Salmon = OldLace = Bisque = BlancheDalmond = Coral = CornSilk =\ 126 | DarkOrange = DeepPink = FloralWhite = Fuchsia = Gold = HotPink = Ivory =\ 127 | LavenderBlush = LemonChiffon = LightPink = LightSalmon = LightYellow = Magenta\ 128 | = MistyRose = Moccasin = NavajoWhite = Orange = OrangeRed = PapayaWhip =\ 129 | PeachPuff = Pink = Red = Seashell = Snow = Tomato = White = Yellow =\ 130 | RebeccaPurple = None 131 | __slots__ = () 132 | 133 | def __new__(cls, *args, a=255, **kwargs): 134 | # Convert CSS-like hexadecimal colour to integer values 135 | if len(args) > 0 and isinstance(args[0], str): 136 | color = args[0] 137 | r, g, b = (int(color[i + 1:i + 3], base=16) for i in range(0, 6, 2)) 138 | return super(Color, cls).__new__(cls, r, g, b, a) 139 | return super(Color, cls).__new__(cls, *args, a=a, **kwargs) 140 | 141 | def alpha(self, value): 142 | """ 143 | Return a new instance of the current colour with a different 144 | alpha value. 145 | 146 | Arguments: 147 | - `value`: An alpha channel value between ``0`` and 148 | ``255``. 149 | """ 150 | return Color(self.r, self.g, self.b, a=value) 151 | 152 | @property 153 | def rgba(self): 154 | """ 155 | Return the instances RGBA values as a tuple. 156 | """ 157 | return tuple(self) 158 | 159 | # Colour shorthands 160 | Color.Aqua = Color(0, 255, 255) 161 | Color.AliceBlue = Color(240, 248, 255) 162 | Color.AntiqueWhite = Color(250, 235, 215) 163 | Color.Black = Color(0, 0, 0) 164 | Color.Blue = Color(0, 0, 255) 165 | Color.Cyan = Color(0, 255, 255) 166 | Color.DarkBlue = Color(0, 0, 139) 167 | Color.DarkCyan = Color(0, 139, 139) 168 | Color.DarkGreen = Color(0, 100, 0) 169 | Color.DarkTurquoise = Color(0, 206, 209) 170 | Color.DeepSkyBlue = Color(0, 191, 255) 171 | Color.Green = Color(0, 128, 0) 172 | Color.Lime = Color(0, 255, 0) 173 | Color.MediumBlue = Color(0, 0, 205) 174 | Color.MediumSpringGreen = Color(0, 250, 154) 175 | Color.Navy = Color(0, 0, 128) 176 | Color.SpringGreen = Color(0, 255, 127) 177 | Color.Teal = Color(0, 128, 128) 178 | Color.MidnightBlue = Color(25, 25, 112) 179 | Color.DodgerBlue = Color(30, 144, 255) 180 | Color.LightSeaGreen = Color(32, 178, 170) 181 | Color.ForestGreen = Color(34, 139, 34) 182 | Color.SeaGreen = Color(46, 139, 87) 183 | Color.DarkSlateGray = Color(47, 79, 79) 184 | Color.DarkSlateGrey = Color(47, 79, 79) 185 | Color.LimeGreen = Color(50, 205, 50) 186 | Color.MediumSeaGreen = Color(60, 179, 113) 187 | Color.Turquoise = Color(64, 224, 208) 188 | Color.RoyalBlue = Color(65, 105, 225) 189 | Color.SteelBlue = Color(70, 130, 180) 190 | Color.DarkSlateBlue = Color(72, 61, 139) 191 | Color.MediumTurquoise = Color(72, 209, 204) 192 | Color.Indigo = Color(75, 0, 130) 193 | Color.DarkOliveGreen = Color(85, 107, 47) 194 | Color.CadetBlue = Color(95, 158, 160) 195 | Color.CornflowerBlue = Color(100, 149, 237) 196 | Color.MediumAquamarine = Color(102, 205, 170) 197 | Color.DimGray = Color(105, 105, 105) 198 | Color.DimGrey = Color(105, 105, 105) 199 | Color.SlateBlue = Color(106, 90, 205) 200 | Color.OliveDrab = Color(107, 142, 35) 201 | Color.SlateGray = Color(112, 128, 144) 202 | Color.SlateGrey = Color(112, 128, 144) 203 | Color.LightSlateGray = Color(119, 136, 153) 204 | Color.LightSlateGrey = Color(119, 136, 153) 205 | Color.MediumSlateBlue = Color(123, 104, 238) 206 | Color.LawnGreen = Color(124, 252, 0) 207 | Color.Aquamarine = Color(127, 255, 212) 208 | Color.Chartreuse = Color(127, 255, 0) 209 | Color.Gray = Color(128, 128, 128) 210 | Color.Grey = Color(128, 128, 128) 211 | Color.Maroon = Color(128, 0, 0) 212 | Color.Olive = Color(128, 128, 0) 213 | Color.Purple = Color(128, 0, 128) 214 | Color.LightSkyBlue = Color(135, 206, 250) 215 | Color.SkyBlue = Color(135, 206, 235) 216 | Color.BlueViolet = Color(138, 43, 226) 217 | Color.DarkMagenta = Color(139, 0, 139) 218 | Color.DarkRed = Color(139, 0, 0) 219 | Color.SaddleBrown = Color(139, 69, 19) 220 | Color.DarkSeaGreen = Color(143, 188, 143) 221 | Color.LightGreen = Color(144, 238, 144) 222 | Color.MediumPurple = Color(147, 112, 219) 223 | Color.DarkViolet = Color(148, 0, 211) 224 | Color.PaleGreen = Color(152, 251, 152) 225 | Color.DarkOrchid = Color(153, 50, 204) 226 | Color.YellowGreen = Color(154, 205, 50) 227 | Color.Sienna = Color(160, 82, 45) 228 | Color.Brown = Color(165, 42, 42) 229 | Color.DarkGray = Color(169, 169, 169) 230 | Color.DarkGrey = Color(169, 169, 169) 231 | Color.GreenYellow = Color(173, 255, 47) 232 | Color.LightBlue = Color(173, 216, 230) 233 | Color.PaleTurquoise = Color(175, 238, 238) 234 | Color.LightSteelBlue = Color(176, 196, 222) 235 | Color.PowderBlue = Color(176, 224, 230) 236 | Color.FireBrick = Color(178, 34, 34) 237 | Color.DarkGoldenrod = Color(184, 134, 11) 238 | Color.MediumOrchid = Color(186, 85, 211) 239 | Color.RosyBrown = Color(188, 143, 143) 240 | Color.DarkKhaki = Color(189, 183, 107) 241 | Color.Silver = Color(192, 192, 192) 242 | Color.MediumVioletRed = Color(199, 21, 133) 243 | Color.IndianRed = Color(205, 92, 92) 244 | Color.Peru = Color(205, 133, 63) 245 | Color.Chocolate = Color(210, 105, 30) 246 | Color.Tan = Color(210, 180, 140) 247 | Color.LightGray = Color(211, 211, 211) 248 | Color.LightGrey = Color(211, 211, 211) 249 | Color.Thistle = Color(216, 191, 216) 250 | Color.Goldenrod = Color(218, 165, 32) 251 | Color.Orchid = Color(218, 112, 214) 252 | Color.PaleVioletRed = Color(219, 112, 147) 253 | Color.Crimson = Color(220, 20, 60) 254 | Color.Gainsboro = Color(220, 220, 220) 255 | Color.Plum = Color(221, 160, 221) 256 | Color.BurlyWood = Color(222, 184, 135) 257 | Color.LightCyan = Color(224, 255, 255) 258 | Color.Lavender = Color(230, 230, 250) 259 | Color.DarkSalmon = Color(233, 150, 122) 260 | Color.PaleGoldenrod = Color(238, 232, 170) 261 | Color.Violet = Color(238, 130, 238) 262 | Color.Azure = Color(240, 255, 255) 263 | Color.Honeydew = Color(240, 255, 240) 264 | Color.Khaki = Color(240, 230, 140) 265 | Color.LightCoral = Color(240, 128, 128) 266 | Color.SandyBrown = Color(244, 164, 96) 267 | Color.Beige = Color(245, 245, 220) 268 | Color.MintCream = Color(245, 255, 250) 269 | Color.Wheat = Color(245, 222, 179) 270 | Color.WhiteSmoke = Color(245, 245, 245) 271 | Color.GhostWhite = Color(248, 248, 255) 272 | Color.LightGoldenrodYellow = Color(250, 250, 210) 273 | Color.Linen = Color(250, 240, 230) 274 | Color.Salmon = Color(250, 128, 114) 275 | Color.OldLace = Color(253, 245, 230) 276 | Color.Bisque = Color(255, 228, 196) 277 | Color.BlancheDalmond = Color(255, 235, 205) 278 | Color.Coral = Color(255, 127, 80) 279 | Color.CornSilk = Color(255, 248, 220) 280 | Color.DarkOrange = Color(255, 140, 0) 281 | Color.DeepPink = Color(255, 20, 147) 282 | Color.FloralWhite = Color(255, 250, 240) 283 | Color.Fuchsia = Color(255, 0, 255) 284 | Color.Gold = Color(255, 215, 0) 285 | Color.HotPink = Color(255, 105, 180) 286 | Color.Ivory = Color(255, 255, 240) 287 | Color.LavenderBlush = Color(255, 240, 245) 288 | Color.LemonChiffon = Color(255, 250, 205) 289 | Color.LightPink = Color(255, 182, 193) 290 | Color.LightSalmon = Color(255, 160, 122) 291 | Color.LightYellow = Color(255, 255, 224) 292 | Color.Magenta = Color(255, 0, 255) 293 | Color.MistyRose = Color(255, 228, 225) 294 | Color.Moccasin = Color(255, 228, 181) 295 | Color.NavajoWhite = Color(255, 222, 173) 296 | Color.Orange = Color(255, 165, 0) 297 | Color.OrangeRed = Color(255, 69, 0) 298 | Color.PapayaWhip = Color(255, 239, 213) 299 | Color.PeachPuff = Color(255, 218, 185) 300 | Color.Pink = Color(255, 192, 203) 301 | Color.Red = Color(255, 0, 0) 302 | Color.Seashell = Color(255, 245, 238) 303 | Color.Snow = Color(255, 250, 250) 304 | Color.Tomato = Color(255, 99, 71) 305 | Color.White = Color(255, 255, 255) 306 | Color.Yellow = Color(255, 255, 0) 307 | Color.RebeccaPurple = Color(102, 51, 153) 308 | 309 | 310 | @enum.unique 311 | class Direction(enum.Enum): 312 | """ 313 | The direction of UI :class:`Container` child elements. 314 | """ 315 | row = 0 316 | column = 1 317 | 318 | 319 | @enum.unique 320 | class Align(enum.Enum): 321 | """ 322 | The alignment of UI :class:`Container` child elements. 323 | """ 324 | left = 0 325 | center = 1 326 | right = 2 327 | 328 | 329 | class Item: 330 | """ 331 | Abstract UI element. 332 | 333 | Arguments: 334 | - `enabled`: Enable or disable the UI element. 335 | - `margin`: The margin of the UI element. Use a 336 | :class:`Distance` instance to provide a margin. 337 | - `padding`: The padding of the UI element. 338 | - `position`: The position of the UI element on the view port. 339 | Defaults to top left of the screen. 340 | - `size`: The size of the UI element. Defaults to zero. 341 | - `color`: The colour of the UI element. Defaults to `black`. 342 | """ 343 | def __init__(self, enabled=True, margin=Distance.Zero, position=Point.TopLeft, 344 | size=Dimension.Zero, color=Color.Black): 345 | self._settings = {} 346 | self.enabled = enabled 347 | self.margin = margin 348 | self.position = position 349 | self.size = size # TODO: Does this need to be applied on the abstract element? 350 | self.color = color 351 | 352 | @property 353 | def enabled(self): 354 | return self._settings.get('enabled') 355 | 356 | @enabled.setter 357 | def enabled(self, value): 358 | self._settings['enabled'] = value 359 | 360 | @property 361 | def margin(self): 362 | return self._settings.get('margin') 363 | 364 | @margin.setter 365 | def margin(self, value): 366 | self._settings['margin'] = value 367 | 368 | @property 369 | def position(self): 370 | return self._settings.get('position') 371 | 372 | @position.setter 373 | def position(self, value): 374 | self._settings['position'] = value 375 | 376 | @property 377 | def size(self): 378 | return self._settings.get('size') 379 | 380 | @size.setter 381 | def size(self, value): 382 | self._settings['size'] = value 383 | 384 | @property 385 | def color(self): 386 | return self._settings.get('color') 387 | 388 | @color.setter 389 | def color(self, value): 390 | self._settings['color'] = value 391 | 392 | def get_coordinates(self, offset): 393 | """ 394 | Return the relative `x` and `y` coordinates. Takes account 395 | to `margin`, it's own `position` and the passed `offset`. 396 | 397 | Arguments: 398 | - `offset`: An offset for the item's position. 399 | """ 400 | x = self.position.x + self.margin.left + offset.x 401 | y = self.position.y + self.margin.top + offset.y 402 | return x, y 403 | 404 | def get_dimension(self): 405 | """ 406 | Return the relative dimension. 407 | """ 408 | return self.size.width, self.size.height 409 | 410 | def draw(self, offset=Point.Zero, **settings): 411 | """ 412 | Draw the element. 413 | 414 | Arguments: 415 | - `offset`: An offset that needs to be considered when 416 | calculating the draw position. 417 | - `settings`: Fallback settings that can be applied when a 418 | property has been set to ``None``. 419 | """ 420 | raise NotImplementedError() 421 | 422 | 423 | # noinspection PyAbstractClass 424 | class SelectableItem(Item): 425 | """ 426 | Abstract selectable UI element. 427 | 428 | Example: A menu item that can be selected by pressing `down` or 429 | `up`. 430 | """ 431 | def select(self): 432 | """ 433 | Called when the element is selected. 434 | """ 435 | raise NotImplementedError() 436 | 437 | def deselect(self): 438 | """ 439 | Called when the element is unselected. 440 | """ 441 | raise NotImplementedError() 442 | 443 | 444 | # noinspection PyAbstractClass 445 | class ActivatableItem(SelectableItem): 446 | """ 447 | Abstract activatable UI element. 448 | 449 | Example: A menu item that can be selected and activated by pressing 450 | `enter`. 451 | """ 452 | def activate(self): 453 | """ 454 | Called when the element has been activated. 455 | """ 456 | raise NotImplementedError() 457 | 458 | 459 | # noinspection PyAbstractClass 460 | class AlterableItem(SelectableItem): 461 | """ 462 | Abstract alterable UI element. 463 | 464 | Example: A menu item with a value that can be selected and altered 465 | by pressing `left` or `right`. 466 | """ 467 | def next(self): 468 | """ 469 | Called when requesting the next value. 470 | """ 471 | raise NotImplementedError() 472 | 473 | def previous(self): 474 | """ 475 | Called when requesting the previous value. 476 | """ 477 | raise NotImplementedError() 478 | 479 | 480 | class Container(Item): 481 | """ 482 | Contains a collection of UI items. 483 | 484 | Arguments: 485 | - `direction`: The direction the UI elements are placed. 486 | - `align`: The alignment of the UI elements. 487 | """ 488 | def __init__(self, *args, direction=Direction.row, align=Align.left, **kwargs): 489 | super().__init__(*args, **kwargs) 490 | self.direction = direction 491 | self.align = align 492 | self._items = [] 493 | 494 | def __iadd__(self, item): 495 | self.add(item) 496 | 497 | def __isub__(self, item): 498 | self.remove(item) 499 | 500 | @property 501 | def direction(self): 502 | return self._settings.get('direction') 503 | 504 | @direction.setter 505 | def direction(self, value): 506 | self._settings['direction'] = value 507 | 508 | @property 509 | def align(self): 510 | return self._settings.get('align') 511 | 512 | @align.setter 513 | def align(self, value): 514 | self._settings['align'] = value 515 | 516 | def add(self, item): 517 | """ 518 | Add an UI item to the container. 519 | 520 | Arguments: 521 | - `item`: The :class:`Item` instance to be added. 522 | """ 523 | self._items.append(item) 524 | 525 | def remove(self, item): 526 | """ 527 | Remove an UI item from the container. 528 | 529 | Arguments: 530 | - `item`: The :class:`Item` instance to be removed. 531 | """ 532 | self._items.append(item) 533 | 534 | def draw(self, offset=Point.Zero, **settings): 535 | """ 536 | Draw the UI elements of the container. 537 | 538 | Arguments: 539 | - `offset`: The starting offset of the UI container. 540 | - `settings`: Fallback settings that will be passed to the 541 | UI elements. 542 | """ 543 | # Override default settings 544 | settings.update(self._settings) 545 | 546 | # Draw each item 547 | i = 0 548 | for item in self._items: 549 | if item.enabled: 550 | # Draw the item when it is enabled with the calculated offset 551 | item_offset = offset + self.get_offset(i, item) 552 | item.draw(offset=item_offset, **settings) 553 | i += 1 554 | 555 | def get_offset(self, nr, item): 556 | """ 557 | Calculate the offset for an UI element. 558 | 559 | Arguments: 560 | - `nr`: The sequence number of the UI element. 561 | - `item`: The UI element :class:`Item` instance. 562 | """ 563 | raise NotImplementedError() 564 | 565 | 566 | def _new_viewport(): 567 | """ 568 | Create and return a new viewport set. 569 | """ 570 | return weakref.WeakSet() 571 | 572 | _viewport = _new_viewport() 573 | 574 | 575 | def add(item): 576 | """ 577 | Add an UI item to the view port. 578 | 579 | .. note:: UI elements from scripts will be automatically removed 580 | when the script returns. But there will be a small delay 581 | until the garbage collector removes the elements. 582 | 583 | Arguments: 584 | - `item`: The :class:`Item` instance to be added. 585 | """ 586 | _viewport.add(item) 587 | 588 | 589 | def remove(item): 590 | """ 591 | Remove an UI item from the view port. 592 | 593 | Arguments: 594 | - `item`: The :class:`Item` instance to be removed. 595 | """ 596 | _viewport.remove(item) 597 | 598 | 599 | def reset(): 600 | """ 601 | Reset the viewport instance. 602 | 603 | .. warning:: Do not call this function from a script! 604 | """ 605 | global _viewport 606 | _viewport = _new_viewport() 607 | 608 | 609 | def draw(): 610 | """ 611 | Draw all UI items on the viewport. 612 | 613 | .. warning:: Do not call this function from a script! 614 | """ 615 | for item in _viewport: 616 | if item.enabled: 617 | item.draw() 618 | -------------------------------------------------------------------------------- /python/gta/ui/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic UI components that use primitives to be drawn. 3 | """ 4 | from gta.ui import ActivatableItem, AlterableItem 5 | from gta.ui.primitive import Rectangle, Label 6 | 7 | __all__ = ('Button', 'Spinner') 8 | 9 | 10 | class Button(ActivatableItem): 11 | pass 12 | 13 | 14 | class Spinner(AlterableItem): 15 | pass 16 | -------------------------------------------------------------------------------- /python/gta/ui/menu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Menu components that can be used to create a menu. 3 | """ 4 | from gta.ui import Container 5 | 6 | __all__ = ('Menu',) 7 | 8 | 9 | class Menu(Container): 10 | pass 11 | -------------------------------------------------------------------------------- /python/gta/ui/primitive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Primitive UI elements. 3 | """ 4 | import gta_native 5 | 6 | from gta import Font 7 | from gta.ui import Item, Point, Color 8 | 9 | __all__ = ('Rectangle', 'Label') 10 | 11 | 12 | class Rectangle(Item): 13 | def draw(self, offset=Point.Zero, **settings): 14 | # TODO: Remove logging 15 | from gta import utils 16 | logger = utils.get_logger('gta.RECTANGLE') 17 | 18 | # Override default settings 19 | settings.update(self._settings) 20 | 21 | # Calculate position and dimension 22 | x, y = self.get_coordinates(offset) 23 | width, height = self.get_dimension() 24 | 25 | text_scale = 0.4 26 | 27 | line_width = 350.0 28 | line_height = 15.0 29 | line_top = 18.0 30 | line_left = 0.0 31 | text_left = 5.0 32 | 33 | line_width_scaled = line_width / 1280 34 | line_top_scaled = line_top / 720 35 | text_left_scaled = text_left / 1280 36 | line_height_scaled = line_height / 720 37 | 38 | line_left_scaled = line_left / 1280 39 | 40 | # Use native functions to draw 41 | logger.warning('x: {}, y: {}, width: {}, height: {}, color: {}', 42 | x, y, width, height, self.color.rgba) 43 | logger.warning('gta_native.graphics.get_screen_resolution()') 44 | gta_native.graphics.get_screen_resolution() 45 | logger.info('Screen resolution: {}', gta_native.graphics.get_screen_resolution()) 46 | logger.warning('gta_native.ui.set_text_font(Font.chalet_london)') 47 | gta_native.ui.set_text_font(Font.chalet_london) 48 | logger.warning('gta_native.ui.set_text_scale(0.0, 0.35)') 49 | gta_native.ui.set_text_scale(0.0, text_scale) 50 | logger.warning('gta_native.ui.set_text_colour(*Color.White.rgba)') 51 | gta_native.ui.set_text_colour(*Color.White.rgba) 52 | logger.warning('gta_native.ui.set_text_centre(True)') 53 | gta_native.ui.set_text_centre(False) 54 | logger.warning('gta_native.ui.set_text_dropshadow(0, 0, 0, 0, 0)') 55 | gta_native.ui.set_text_dropshadow(0, 0, 0, 0, 0) 56 | logger.warning('gta_native.ui.set_text_edge(0, 0, 0, 0, 0)') 57 | gta_native.ui.set_text_edge(0, 0, 0, 0, 0) 58 | logger.warning('gta_native.ui._SET_TEXT_ENTRY(\'STRING\')') 59 | gta_native.ui._SET_TEXT_ENTRY('STRING') 60 | logger.warning('gta_native.ui._ADD_TEXT_COMPONENT_STRING(\'TEST\')') 61 | gta_native.ui._ADD_TEXT_COMPONENT_STRING('TEST') 62 | logger.warning('gta_native.ui._DRAW_TEXT(text_left_scaled, no_idea_1)') 63 | no_idea_1 = line_top_scaled + 0.00278 + line_height_scaled - 0.005 64 | logger.info('text_left_scaled={}, no_idea_1={}', text_left_scaled, no_idea_1) 65 | gta_native.ui._DRAW_TEXT(text_left_scaled, no_idea_1) 66 | no_idea_2 = gta_native.ui._0xDB88A37483346780(text_scale, 0) 67 | logger.info('line_left_scaled={}, line_top_scaled + 0.00278={}, line_width_scaled={}, no_idea_2 + line_height_scaled*2.0 + 0.005={}, *self.color.rgba={}', 68 | line_left_scaled, line_top_scaled + 0.00278, line_width_scaled, no_idea_2 + line_height_scaled*2.0 + 0.005, *self.color.rgba) 69 | gta_native.graphics.draw_rect( 70 | line_left_scaled, 71 | line_top_scaled + 0.00278, 72 | line_width_scaled, 73 | no_idea_2 + line_height_scaled*2.0 + 0.005, 74 | *self.color.rgba 75 | ) 76 | # gta_native.graphics.draw_rect(x, y, width, height, *self.color.rgba) 77 | 78 | class Label(Item): 79 | pass 80 | -------------------------------------------------------------------------------- /python/gta/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pip.commands 4 | import pip.exceptions 5 | import pkg_resources 6 | 7 | from gta.exceptions import * 8 | 9 | __all__ = ('Message', 'CurlyBracketFormattingAdapter', 'get_directory', 'setup_logging', 10 | 'get_logger', 'install_dependency') 11 | 12 | 13 | class Message: 14 | """ 15 | A wrapper class that applies the new formatting style on a message 16 | and it's arguments. 17 | 18 | It's safe to throw any object in here that has a __str__ method 19 | (e.g. an exception). 20 | 21 | ..note:: Using keywords in the formatter will not work. 22 | 23 | Arguments: 24 | - `fmt`: A formatter string or an object that has a __str__ 25 | method. 26 | - `args`: Arguments that will be passed to the formatter. 27 | """ 28 | 29 | def __init__(self, fmt, args): 30 | """Create a message instance with formatter and arguments.""" 31 | self._fmt = fmt 32 | self._args = args 33 | 34 | def __str__(self): 35 | """ 36 | Return a formatted string using curly brackets. 37 | 38 | The __str__ method will be called if :attr:`_fmt` is not a 39 | string. 40 | """ 41 | if isinstance(self._fmt, str): 42 | return self._fmt.format(*self._args) 43 | else: 44 | return self._fmt.__str__() 45 | 46 | 47 | class CurlyBracketFormattingAdapter(logging.LoggerAdapter): 48 | """ 49 | A logging style adapter that is able to use the new curly bracket 50 | formatting style. 51 | 52 | Arguments: 53 | - `logger`: Instance of :class:`logging.Logger`. 54 | - `extra`: Optional dict-like object that will be passed to 55 | every log message and can be used for formatting. 56 | """ 57 | def __init__(self, logger, extra=None): 58 | super().__init__(logger, extra or {}) 59 | 60 | def log(self, level, msg, *args, **kwargs): 61 | """ 62 | Pass a log message. Shouldn't be called directly. Use level 63 | methods instead (e.g. info, warning, etc.). 64 | """ 65 | if self.isEnabledFor(level): 66 | msg, kwargs = self.process(msg, kwargs) 67 | # noinspection PyProtectedMember 68 | self.logger._log(level, Message(msg, args), (), **kwargs) 69 | 70 | 71 | def get_directory(): 72 | return os.path.abspath(os.path.join(os.getcwd(), 'python')) 73 | 74 | 75 | def setup_logging(console): 76 | """ 77 | Setup logging formatter, handlers, etc. for the `gta` and `pip` 78 | logger. 79 | 80 | Arguments: 81 | - `console`: Use console logging instead of file logging. 82 | """ 83 | # Setup formatter and handler 84 | formatter = logging.Formatter( 85 | fmt='{asctime} {name:<22} {levelname:<18} {message}', 86 | datefmt='%Y-%m-%d %H:%M:%S', 87 | style='{' 88 | ) 89 | 90 | # Output in file or using the console 91 | if console: 92 | handler = logging.StreamHandler() 93 | handler.setFormatter(formatter) 94 | else: 95 | handler = logging.FileHandler('scripthookvpy3k.log') 96 | handler.setFormatter(formatter) 97 | 98 | # Redirect warnings to the logger 99 | logging.captureWarnings(True) 100 | 101 | # Setup loggers 102 | loggers = ( 103 | ('gta', logging.DEBUG), 104 | ('py.warnings', logging.WARNING), 105 | ('asyncio', logging.INFO), 106 | ('pip', logging.WARNING) 107 | ) 108 | for name, level in loggers: 109 | logger = logging.getLogger(name) 110 | logger.setLevel(level) 111 | logger.addHandler(handler) 112 | 113 | 114 | def get_logger(name='gta'): 115 | """ 116 | Wrap the curly bracket formatting adapter around a logger. Should 117 | always be used instead of ``logging.getLogger``. 118 | 119 | Arguments: 120 | - `name`: The name of the logger. 121 | 122 | Return the wrapped :class:`logging.logger` instance. 123 | """ 124 | return CurlyBracketFormattingAdapter(logging.getLogger(name)) 125 | 126 | 127 | dependencies_blacklist = {'aiohttp', 'numpy', 'scipy'} 128 | 129 | 130 | def install_dependency(dependency): 131 | """ 132 | Install a dependency using :class:`pip`. 133 | 134 | Arguments: 135 | - `dependency`: A dependency as a `requirement specifier 136 | `_. 137 | - `use_script_path`: Install the dependency into the specified 138 | directory instead of the scripts main directory. 139 | """ 140 | logger = get_logger() 141 | 142 | # Get path 143 | path = os.path.abspath(get_directory()) 144 | 145 | try: 146 | # Check if dependency is satisfied 147 | pkg_resources.require(dependency) 148 | except pkg_resources.ResolutionError: 149 | # Check if dependency is blacklisted 150 | for requirement in pkg_resources.parse_requirements(dependency): 151 | if requirement.project_name in dependencies_blacklist: 152 | raise DependencyBlacklistedError(dependency) 153 | 154 | try: 155 | # Install dependency 156 | message = 'Installing dependency "{}" into path "{}"' 157 | logger.debug(message, dependency, os.path.relpath(path)) 158 | command = pip.commands.InstallCommand(isolated=True) 159 | # Note: We can't run 'main' because it overrides our logging settings 160 | options, args = command.parse_args([ 161 | '--disable-pip-version-check', 162 | '--upgrade', 163 | '--target', path, 164 | dependency 165 | ]) 166 | command.run(options, args) 167 | except pip.exceptions.PipError as exc: 168 | raise InstallDependencyError(dependency) from exc 169 | else: 170 | logger.debug('Dependency "{}" already satisfied', dependency) 171 | -------------------------------------------------------------------------------- /python/scripts/helper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import gta.utils 4 | 5 | __author__ = 'Lennart Grahl ' 6 | __status__ = 'Development' 7 | __version__ = '0.0.1' 8 | 9 | 10 | @asyncio.coroutine 11 | def main(): 12 | """ 13 | Various tools that should help scripters doing their work. 14 | """ 15 | logger = gta.utils.get_logger('gta.helper') 16 | -------------------------------------------------------------------------------- /python/scripts/metadata.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import gta.utils 4 | 5 | # The following metadata will not be processed but is recommended 6 | # Author name and E-Mail 7 | __author__ = 'Full Name ' 8 | # Status of the script: Use one of 'Prototype', 'Development', 'Production' 9 | __status__ = 'Development' 10 | 11 | # The following metadata will be parsed and should always be provided 12 | # Version number: This should always be a string and formatted in the x.x.x notation 13 | __version__ = '0.0.1' 14 | # A list of dependencies in the requirement specifiers format 15 | # See: https://pip.pypa.io/en/latest/reference/pip_install.html#requirement-specifiers 16 | __dependencies__ = ('aiohttp>=0.15.3',) 17 | 18 | 19 | @asyncio.coroutine 20 | def main(): 21 | """ 22 | Does absolutely nothing but show you how to provide metadata. 23 | """ 24 | logger = gta.utils.get_logger('gta.metadata') 25 | logger.debug('Hello from the metadata example') 26 | -------------------------------------------------------------------------------- /python/scripts/running.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import gta.utils 4 | 5 | __author__ = 'Lennart Grahl ' 6 | __status__ = 'Production' 7 | __version__ = '1.0.0' 8 | 9 | 10 | @asyncio.coroutine 11 | def main(): 12 | """ 13 | Yields every second and shouts at the logger. 14 | Also tries to ignore cancel events but fails in the end. 15 | """ 16 | logger = gta.utils.get_logger('gta.running') 17 | counter = 0 18 | while True: 19 | try: 20 | logger.debug("{} times and counting!", counter) 21 | yield from asyncio.sleep(1.0) 22 | counter += 1 23 | except asyncio.CancelledError: 24 | logger.debug('Are you trying to shut me down?') 25 | -------------------------------------------------------------------------------- /python/scripts/ui.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from gta import Key, ui, utils 4 | from gta.events import key 5 | from gta.ui import Dimension, primitive 6 | 7 | __author__ = 'Lennart Grahl ' 8 | __status__ = 'Development' 9 | __version__ = '0.0.1' 10 | 11 | 12 | @asyncio.coroutine 13 | def main(): 14 | """ 15 | Creates a few primitive UI elements. 16 | """ 17 | logger = utils.get_logger('gta.ui') 18 | counter = 0 19 | 20 | # Create primitive UI elements 21 | rectangle = primitive.Rectangle(size=Dimension.Quarter) 22 | ui.add(rectangle) 23 | 24 | while True: 25 | yield from key(codes=Key.F12) 26 | ui.draw() 27 | 28 | # Modify the created UI elements 29 | # TODO 30 | 31 | counter += 1 32 | -------------------------------------------------------------------------------- /python/scripts/vehicle_color.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | import gta_native 5 | 6 | from gta import utils 7 | from gta.events import wait 8 | from gta.requires import player 9 | 10 | __author__ = 'Lennart Grahl ' 11 | __status__ = 'Production' 12 | __version__ = '1.1.1' 13 | 14 | 15 | @asyncio.coroutine 16 | def main(): 17 | """ 18 | Applies a different vehicle colour every second. 19 | """ 20 | logger = utils.get_logger('gta.vehicle_color') 21 | 22 | while True: 23 | # Wait until the player is in a vehicle with a precision of 100 ticks 24 | vehicle = yield from wait(player.get_vehicle, precision=100) 25 | # Generate colour 26 | color = [random.randint(0, 255) for _ in range(3)] 27 | # Apply colour 28 | logger.debug('Changing vehicle color to: {}', color) 29 | gta_native.vehicle.set_vehicle_custom_primary_colour(vehicle, *color) 30 | # Because the coroutine usually returns immediately, we want to 31 | # wait a second, so the script isn't spamming colours every tick 32 | yield from asyncio.sleep(1.0) 33 | -------------------------------------------------------------------------------- /python/scripts/wanted.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import gta 4 | import gta_native 5 | 6 | from gta import Key, utils 7 | from gta.events import key 8 | from gta.requires import player 9 | 10 | __author__ = 'Lennart Grahl ' 11 | __status__ = 'Production' 12 | __version__ = '2.0.0' 13 | 14 | 15 | @asyncio.coroutine 16 | def main(): 17 | """ 18 | Increase the wanted level when `+` is pressed. 19 | Decrease the wanted level when `-` is pressed. 20 | Double increase or decrease when `Shift` is pressed as well. 21 | """ 22 | logger = utils.get_logger('gta.wanted') 23 | 24 | while True: 25 | # Wait until '+' or '-' has been pressed 26 | code, _, modifiers = yield from key(codes={Key.ADD, Key.SUBTRACT}) 27 | number = 2 if modifiers['shift'] else 1 28 | 29 | try: 30 | # Get player id and wanted level 31 | player_id = player.get_id() 32 | wanted_level = gta_native.player.get_player_wanted_level(player_id) 33 | logger.debug('Wanted level is {}', wanted_level) 34 | 35 | # Increase or decrease wanted level 36 | if code == Key.ADD and wanted_level < 5: 37 | wanted_level = min(wanted_level + number, 5) 38 | logger.debug('Increasing wanted level to {}', wanted_level) 39 | elif code == Key.SUBTRACT and wanted_level > 0: 40 | wanted_level = max(wanted_level - number, 0) 41 | logger.debug('Decreasing wanted level to {}', wanted_level) 42 | 43 | # Apply wanted level 44 | gta_native.player.set_player_wanted_level(player_id, wanted_level, False) 45 | gta_native.player.set_player_wanted_level_now(player_id, False) 46 | except gta.RequirementError as exc: 47 | logger.debug(exc) 48 | -------------------------------------------------------------------------------- /python/scripts/weather_time.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | import gta.utils 5 | import gta_native 6 | import aiohttp 7 | 8 | __author__ = 'Lennart Grahl ' 9 | __status__ = 'Development' 10 | __version__ = '0.9.1' 11 | __dependencies__ = ('aiohttp>=0.15.3',) 12 | 13 | 14 | @asyncio.coroutine 15 | def main(): 16 | """ 17 | Applies the current weather from Los Angeles in game. 18 | """ 19 | url = 'http://api.openweathermap.org/data/2.5/weather?id=3882428' 20 | logger = gta.utils.get_logger('gta.weather-time') 21 | map_weather_id = { 22 | 'WIND': (900, 905, 771), 23 | 'EXTRASUNNY': (904,), 24 | 'CLEAR': (800, 801), 25 | 'CLOUDS': (802, 803, 804), 26 | 'SMOG': (711, 751, 761, 762), 27 | 'FOGGY': (701, 721, 731, 741), 28 | 'OVERCAST': (), 29 | 'RAIN': (500, 501, 502, 503, 504, 511, 520, 521, 522, 531, 311, 312, 313, 314, 30 | 321), 31 | 'THUNDER': (200, 201, 202, 210, 211, 212, 221, 230, 231, 232, 900, 901, 902, 781), 32 | 'CLEARING': (300, 301, 302, 310), 33 | 'NEUTRAL': (), 34 | 'SNOW': (903, 906, 601, 611, 612, 615, 622), 35 | 'BLIZZARD': (602, 621), 36 | 'SNOWLIGHT': (600, 616, 620), 37 | 'XMAS': () 38 | } 39 | 40 | while True: 41 | # Get weather in Los Angeles 42 | logger.debug('Requesting {}', url) 43 | response = yield from aiohttp.request('get', url) 44 | try: 45 | json = yield from response.json() 46 | except ValueError: 47 | logger.warning('Parsing response failed') 48 | continue 49 | logger.debug('Response: {}', json) 50 | 51 | # Calculate weather type 52 | weather_types = {} 53 | try: 54 | # Map OpenWeatherMap IDs to GTA weather types and apply a rating 55 | for weather in json['weather']: 56 | id_ = weather['id'] 57 | for weather_type, ids in map_weather_id.items(): 58 | if id_ in ids: 59 | weather_types.setdefault(weather_type, 0) 60 | weather_types[weather_type] += 1 61 | 62 | # Get the weather types with the highest rating 63 | highest = max(weather_types.values()) 64 | weather_types = [weather_type for weather_type, count in weather_types.items() 65 | if count == highest] 66 | 67 | # When there are multiple weather types with the same rating, choose randomly 68 | if len(weather_types) > 1: 69 | logger.debug('Randomly choosing from: {}', weather_types) 70 | weather = random.choice(weather_types) 71 | else: 72 | weather, *_ = weather_types 73 | except (ValueError, KeyError): 74 | logger.warning('Could not parse weather data') 75 | yield from asyncio.sleep(10.0) 76 | continue 77 | 78 | # Apply weather type in GTA 79 | logger.info('Setting weather to: {}', weather) 80 | gta_native.gameplay.set_weather_type_now_persist(weather) 81 | gta_native.gameplay.clear_weather_type_persist() 82 | 83 | # TODO: Set wind 84 | # gta_native.gameplay.set_wind(1.0) 85 | # gta_native.gameplay.set_wind_speed(11.99) 86 | # gta_native.gameplay.set_wind_direction(gta_native.entity.get_entity_heading(gta_native.player.player_ped_id())) 87 | 88 | # TODO: Set time 89 | 90 | # Wait for a minute 91 | yield from asyncio.sleep(60.0) 92 | -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /swig/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tools/generate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse natives.h, generate wrapper with SWIG and apply namespaces in 3 | the generated wrapper. 4 | """ 5 | import os 6 | import subprocess 7 | import io 8 | 9 | from distutils.version import StrictVersion 10 | import sys 11 | 12 | path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..') 13 | natives_h = os.path.join(path, 'sdk', 'inc', 'natives.h') 14 | natives_i = os.path.join(path, 'cpp', 'src', 'natives.i') 15 | gta_native_py_in = os.path.join(path, 'cpp', 'src', 'gta_native.py') 16 | gta_native_py_out = os.path.join(path, 'python', 'gta_native.py') 17 | 18 | swig_path = os.path.join(path, 'swig') 19 | swig_version = ['swig', '-version'] 20 | swig_generate = ['swig', '-python', '-c++', natives_i] 21 | swig_required_version = '3.0.5' 22 | 23 | 24 | def main(): 25 | # Change to SWIG path 26 | os.chdir(swig_path) 27 | 28 | # Check SWIG version 29 | for line in io.StringIO(subprocess.check_output(swig_version).decode('utf-8')): 30 | if line.startswith('SWIG Version '): 31 | *init, version = line.split('SWIG Version ') 32 | version = StrictVersion(version.strip()) 33 | if version >= StrictVersion(swig_required_version): 34 | print('SWIG Version:', version) 35 | break 36 | else: 37 | fail('SWIG Version >= {} required'.format(swig_required_version), 2) 38 | 39 | # Map namespaces to function names 40 | date = 'Unknown' 41 | functions = {} 42 | namespace = 'default' 43 | print('Mapping namespaces to functions') 44 | with open(natives_h) as natives: 45 | for line in natives: 46 | line = line.strip() 47 | 48 | # Date 49 | if line.startswith('// Generated'): 50 | _, date_ = line.split('// Generated') 51 | date = date_.strip() 52 | 53 | # Namespace 54 | if line.startswith('namespace'): 55 | head, namespace, *tail = line.split(' ') 56 | namespace = namespace.lower() 57 | 58 | # Function 59 | if line.startswith('static'): 60 | *init, last = line.split(' ', maxsplit=2) 61 | name, *tail = last.split('(', maxsplit=1) 62 | functions.setdefault(namespace, set()) 63 | functions[namespace].add(name) 64 | 65 | # Generate wrapper 66 | print('Generating wrapper') 67 | try: 68 | subprocess.check_call(swig_generate) 69 | except subprocess.CalledProcessError as exc: 70 | fail(exc, 3) 71 | 72 | # Rewrite Python wrapper 73 | last_namespace = None 74 | function_found = False 75 | skip = 0 76 | indent = ' ' * 4 77 | init = [] 78 | middle = ["__version__ = '{}'\n\n\n".format(date.strip("'"))] 79 | tail = [] 80 | 81 | def add_normal(_line): 82 | if function_found: 83 | tail.append(_line) 84 | else: 85 | init.append(_line) 86 | 87 | def add_class_assignment(): 88 | if last_namespace is not None: 89 | middle.append('{} = _{}\n\n\n'.format( 90 | last_namespace, last_namespace.capitalize())) 91 | 92 | def maybe_add_method(_line): 93 | nonlocal last_namespace, function_found 94 | for _namespace, names in functions.items(): 95 | for _name in names: 96 | if _line.startswith('def {}('.format(_name)): 97 | function_found = True 98 | if _namespace != last_namespace: 99 | # Insert class assignment at the end of a namespace 100 | add_class_assignment() 101 | 102 | # Insert class declaration at the start of a namespace 103 | middle.append('class _{}(_object):\n'.format(_namespace.capitalize())) 104 | last_namespace = _namespace 105 | 106 | # Insert staticmethod and function definition 107 | *_, _last = _line.split('(', maxsplit=1) 108 | middle.append(indent + '@staticmethod\n') 109 | middle.append(indent + 'def {}({}'.format( 110 | _name if _name.startswith('_') else _name.lower(), 111 | _last 112 | )) 113 | return 3 114 | add_normal(_line) 115 | return 0 116 | 117 | # Parse generated Python wrapper 118 | print('Parsing generated Python wrapper') 119 | with open(gta_native_py_in) as natives_in: 120 | for line in natives_in: 121 | if skip > 0: 122 | # Return statement 123 | if skip == 3: 124 | middle.append('{}{}\n'.format(indent, line)) 125 | elif skip == 1 and len(line.strip()) > 0: 126 | add_normal(line) 127 | skip -= 1 128 | elif line.startswith('def '): 129 | # Function 130 | skip = maybe_add_method(line) 131 | else: 132 | # Something else 133 | add_normal(line) 134 | add_class_assignment() 135 | 136 | # Write new Python wrapper 137 | print('Writing new Python wrapper') 138 | with open(gta_native_py_out, 'w') as natives_out: 139 | natives_out.writelines(init + middle + tail) 140 | 141 | # Remove originally generated Python wrapper 142 | print('Removing originally generated Python wrapper') 143 | os.remove(gta_native_py_in) 144 | 145 | # Done 146 | print('Done') 147 | sys.exit(0) 148 | 149 | 150 | def fail(message, status): 151 | print(message, file=sys.stderr) 152 | sys.exit(status) 153 | 154 | if __name__ == '__main__': 155 | main() 156 | -------------------------------------------------------------------------------- /tools/simulate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import unittest.mock 4 | import time 5 | 6 | 7 | class GTANativeMock(unittest.mock.MagicMock): 8 | pass 9 | 10 | # Create fake native module 11 | sys.modules['_gta_native'] = GTANativeMock() 12 | import gta 13 | 14 | 15 | # noinspection PyProtectedMember 16 | def main(seconds=5.0): 17 | def sleep(remaining): 18 | nonlocal seconds 19 | seconds -= remaining 20 | time.sleep(remaining) 21 | 22 | # We are running this from the 'python' directory, so we need to go up 23 | os.chdir('../') 24 | 25 | # Initialise 26 | gta._init(console=True) 27 | gta._tick() 28 | sleep(1.0) 29 | 30 | # Inject some keys and ticks 31 | gta._tick() 32 | gta._key(gta.Key.ADD.value, False, alt=False, ctrl=False, shift=False) 33 | sleep(0.2) 34 | gta._tick() 35 | sleep(0.3) 36 | gta._key(gta.Key.ADD.value, False, alt=False, ctrl=False, shift=True) 37 | gta._tick() 38 | sleep(0.5) 39 | gta._tick() 40 | gta._key(gta.Key.SUBTRACT.value, False, alt=False, ctrl=False, shift=False) 41 | sleep(0.33) 42 | while seconds > 0: 43 | gta._tick() 44 | sleep(0.25) 45 | 46 | # Stop 47 | gta._exit() 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | --------------------------------------------------------------------------------