├── .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 |
--------------------------------------------------------------------------------