aether-engine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aether/__init__.py +54 -0
- aether/audio/__init__.py +0 -0
- aether/audio/audio_manager.py +211 -0
- aether/audio/music.py +6 -0
- aether/audio/sound.py +6 -0
- aether/core/__init__.py +211 -0
- aether/core/achievement_system.py +304 -0
- aether/core/application.py +19 -0
- aether/core/config.py +75 -0
- aether/core/console.py +366 -0
- aether/core/engine.py +50 -0
- aether/core/event_bus.py +24 -0
- aether/core/input.py +58 -0
- aether/core/plugin_system.py +93 -0
- aether/core/save_system.py +409 -0
- aether/core/time_manager.py +25 -0
- aether/core/window.py +57 -0
- aether/graphics/__init__.py +0 -0
- aether/graphics/camera.py +69 -0
- aether/graphics/framebuffer.py +119 -0
- aether/graphics/light.py +32 -0
- aether/graphics/lod_system.py +170 -0
- aether/graphics/material.py +25 -0
- aether/graphics/mesh.py +83 -0
- aether/graphics/mesh_factory.py +67 -0
- aether/graphics/particle_system.py +268 -0
- aether/graphics/post_process.py +279 -0
- aether/graphics/renderer.py +135 -0
- aether/graphics/shader.py +61 -0
- aether/graphics/shader_library.py +157 -0
- aether/graphics/sky_atmosphere.py +309 -0
- aether/graphics/terrain.py +408 -0
- aether/graphics/texture.py +34 -0
- aether/graphics/water.py +432 -0
- aether/physics/__init__.py +0 -0
- aether/physics/bullet_backend.py +395 -0
- aether/physics/collider.py +22 -0
- aether/physics/physics_world.py +436 -0
- aether/physics/raycast.py +25 -0
- aether/physics/rigidbody.py +7 -0
- aether/resources/__init__.py +0 -0
- aether/resources/asset_database.py +76 -0
- aether/resources/resource_manager.py +147 -0
- aether/resources/serialization.py +114 -0
- aether/scene/__init__.py +0 -0
- aether/scene/component.py +368 -0
- aether/scene/entity.py +46 -0
- aether/scene/scene.py +33 -0
- aether/scene/scene_graph.py +252 -0
- aether/scene/scene_manager.py +191 -0
- aether/ui/__init__.py +0 -0
- aether/ui/button.py +297 -0
- aether/ui/canvas.py +11 -0
- aether/ui/text.py +286 -0
- aether/utils/__init__.py +0 -0
- aether/utils/logger.py +16 -0
- aether/utils/math_utils.py +31 -0
- aether/utils/object_pool.py +137 -0
- aether/utils/profiler.py +187 -0
- aether_engine-1.0.0.dist-info/METADATA +71 -0
- aether_engine-1.0.0.dist-info/RECORD +64 -0
- aether_engine-1.0.0.dist-info/WHEEL +5 -0
- aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- aether_engine-1.0.0.dist-info/top_level.txt +1 -0
aether/core/console.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Система консольных команд"""
|
|
2
|
+
from typing import Dict, List, Callable, Any, Optional
|
|
3
|
+
import inspect
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConsoleCommand:
|
|
8
|
+
"""Консольная команда"""
|
|
9
|
+
|
|
10
|
+
def __init__(self,
|
|
11
|
+
name: str,
|
|
12
|
+
func: Callable,
|
|
13
|
+
description: str = "",
|
|
14
|
+
usage: str = "",
|
|
15
|
+
category: str = "General"):
|
|
16
|
+
self.name = name
|
|
17
|
+
self.func = func
|
|
18
|
+
self.description = description
|
|
19
|
+
self.usage = usage or name
|
|
20
|
+
self.category = category
|
|
21
|
+
|
|
22
|
+
def execute(self, args: List[str]) -> str:
|
|
23
|
+
"""Выполняет команду"""
|
|
24
|
+
try:
|
|
25
|
+
return str(self.func(*args))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
return f"Error: {e}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleVariable:
|
|
31
|
+
"""Консольная переменная"""
|
|
32
|
+
|
|
33
|
+
def __init__(self,
|
|
34
|
+
name: str,
|
|
35
|
+
default_value: Any,
|
|
36
|
+
description: str = "",
|
|
37
|
+
min_value: Optional[float] = None,
|
|
38
|
+
max_value: Optional[float] = None,
|
|
39
|
+
callback: Optional[Callable] = None):
|
|
40
|
+
self.name = name
|
|
41
|
+
self._value = default_value
|
|
42
|
+
self.default_value = default_value
|
|
43
|
+
self.description = description
|
|
44
|
+
self.min_value = min_value
|
|
45
|
+
self.max_value = max_value
|
|
46
|
+
self.callback = callback
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def value(self) -> Any:
|
|
50
|
+
return self._value
|
|
51
|
+
|
|
52
|
+
@value.setter
|
|
53
|
+
def value(self, new_value: Any):
|
|
54
|
+
# Проверка типа
|
|
55
|
+
if isinstance(self._value, bool):
|
|
56
|
+
if isinstance(new_value, str):
|
|
57
|
+
new_value = new_value.lower() in ('true', '1', 'yes', 'on')
|
|
58
|
+
else:
|
|
59
|
+
new_value = bool(new_value)
|
|
60
|
+
elif isinstance(self._value, int):
|
|
61
|
+
new_value = int(new_value)
|
|
62
|
+
elif isinstance(self._value, float):
|
|
63
|
+
new_value = float(new_value)
|
|
64
|
+
|
|
65
|
+
# Проверка диапазона
|
|
66
|
+
if isinstance(new_value, (int, float)):
|
|
67
|
+
if self.min_value is not None:
|
|
68
|
+
new_value = max(self.min_value, new_value)
|
|
69
|
+
if self.max_value is not None:
|
|
70
|
+
new_value = min(self.max_value, new_value)
|
|
71
|
+
|
|
72
|
+
old_value = self._value
|
|
73
|
+
self._value = new_value
|
|
74
|
+
|
|
75
|
+
# Вызываем callback
|
|
76
|
+
if self.callback and old_value != new_value:
|
|
77
|
+
self.callback(old_value, new_value)
|
|
78
|
+
|
|
79
|
+
def toggle(self):
|
|
80
|
+
"""Переключает булеву переменную"""
|
|
81
|
+
if isinstance(self._value, bool):
|
|
82
|
+
self.value = not self._value
|
|
83
|
+
|
|
84
|
+
def reset(self):
|
|
85
|
+
"""Сбрасывает к значению по умолчанию"""
|
|
86
|
+
self.value = self.default_value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Console:
|
|
90
|
+
"""Консоль движка"""
|
|
91
|
+
|
|
92
|
+
_instance = None
|
|
93
|
+
|
|
94
|
+
def __new__(cls):
|
|
95
|
+
if cls._instance is None:
|
|
96
|
+
cls._instance = super().__new__(cls)
|
|
97
|
+
cls._instance._initialize()
|
|
98
|
+
return cls._instance
|
|
99
|
+
|
|
100
|
+
def _initialize(self):
|
|
101
|
+
"""Инициализация консоли"""
|
|
102
|
+
self._commands: Dict[str, ConsoleCommand] = {}
|
|
103
|
+
self._variables: Dict[str, ConsoleVariable] = {}
|
|
104
|
+
self._history: List[str] = []
|
|
105
|
+
self._history_index = -1
|
|
106
|
+
self._max_history = 100
|
|
107
|
+
|
|
108
|
+
# Состояние
|
|
109
|
+
self.visible = False
|
|
110
|
+
self._input_buffer = ""
|
|
111
|
+
self._output_lines: List[str] = []
|
|
112
|
+
self._max_output_lines = 200
|
|
113
|
+
|
|
114
|
+
# Регистрируем встроенные команды
|
|
115
|
+
self._register_default_commands()
|
|
116
|
+
self._register_default_variables()
|
|
117
|
+
|
|
118
|
+
print("[Console] Initialized")
|
|
119
|
+
|
|
120
|
+
def _register_default_commands(self):
|
|
121
|
+
"""Регистрирует стандартные команды"""
|
|
122
|
+
self.register_command("help", self._cmd_help, "Shows available commands")
|
|
123
|
+
self.register_command("list", self._cmd_list, "Lists all commands and variables")
|
|
124
|
+
self.register_command("clear", self._cmd_clear, "Clears the console")
|
|
125
|
+
self.register_command("echo", self._cmd_echo, "Prints a message")
|
|
126
|
+
self.register_command("quit", self._cmd_quit, "Exits the engine")
|
|
127
|
+
self.register_command("fps", self._cmd_fps, "Shows FPS")
|
|
128
|
+
self.register_command("time_scale", self._cmd_time_scale, "Sets time scale")
|
|
129
|
+
|
|
130
|
+
def _register_default_variables(self):
|
|
131
|
+
"""Регистрирует стандартные переменные"""
|
|
132
|
+
self.register_variable("r_wireframe", False, "Toggle wireframe mode")
|
|
133
|
+
self.register_variable("r_vsync", True, "Toggle VSync")
|
|
134
|
+
self.register_variable("r_msaa", 4, "MSAA samples", 0, 16)
|
|
135
|
+
self.register_variable("g_gravity", -9.81, "Gravity value")
|
|
136
|
+
self.register_variable("g_time_scale", 1.0, "Time scale", 0.0, 10.0)
|
|
137
|
+
self.register_variable("dbg_show_colliders", False, "Show collider debug")
|
|
138
|
+
self.register_variable("dbg_show_fps", True, "Show FPS counter")
|
|
139
|
+
self.register_variable("s_volume_master", 1.0, "Master volume", 0.0, 1.0)
|
|
140
|
+
self.register_variable("s_volume_sfx", 1.0, "SFX volume", 0.0, 1.0)
|
|
141
|
+
self.register_variable("s_volume_music", 0.8, "Music volume", 0.0, 1.0)
|
|
142
|
+
|
|
143
|
+
def register_command(self,
|
|
144
|
+
name: str,
|
|
145
|
+
func: Callable,
|
|
146
|
+
description: str = "",
|
|
147
|
+
usage: str = "",
|
|
148
|
+
category: str = "General"):
|
|
149
|
+
"""Регистрирует команду"""
|
|
150
|
+
cmd = ConsoleCommand(name, func, description, usage, category)
|
|
151
|
+
self._commands[name.lower()] = cmd
|
|
152
|
+
|
|
153
|
+
def unregister_command(self, name: str):
|
|
154
|
+
"""Удаляет команду"""
|
|
155
|
+
if name.lower() in self._commands:
|
|
156
|
+
del self._commands[name.lower()]
|
|
157
|
+
|
|
158
|
+
def register_variable(self,
|
|
159
|
+
name: str,
|
|
160
|
+
default_value: Any,
|
|
161
|
+
description: str = "",
|
|
162
|
+
min_value: Optional[float] = None,
|
|
163
|
+
max_value: Optional[float] = None,
|
|
164
|
+
callback: Optional[Callable] = None):
|
|
165
|
+
"""Регистрирует переменную"""
|
|
166
|
+
var = ConsoleVariable(name, default_value, description,
|
|
167
|
+
min_value, max_value, callback)
|
|
168
|
+
self._variables[name.lower()] = var
|
|
169
|
+
|
|
170
|
+
def get_variable(self, name: str) -> Any:
|
|
171
|
+
"""Получает значение переменной"""
|
|
172
|
+
var = self._variables.get(name.lower())
|
|
173
|
+
return var.value if var else None
|
|
174
|
+
|
|
175
|
+
def set_variable(self, name: str, value: Any):
|
|
176
|
+
"""Устанавливает значение переменной"""
|
|
177
|
+
var = self._variables.get(name.lower())
|
|
178
|
+
if var:
|
|
179
|
+
var.value = value
|
|
180
|
+
self._output(f"Set {name} = {var.value}")
|
|
181
|
+
else:
|
|
182
|
+
self._output(f"Unknown variable: {name}")
|
|
183
|
+
|
|
184
|
+
def execute(self, input_str: str) -> str:
|
|
185
|
+
"""Выполняет команду или устанавливает переменную"""
|
|
186
|
+
if not input_str.strip():
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
# Добавляем в историю
|
|
190
|
+
self._history.append(input_str)
|
|
191
|
+
if len(self._history) > self._max_history:
|
|
192
|
+
self._history.pop(0)
|
|
193
|
+
self._history_index = len(self._history)
|
|
194
|
+
|
|
195
|
+
# Парсим
|
|
196
|
+
try:
|
|
197
|
+
parts = shlex.split(input_str)
|
|
198
|
+
except ValueError:
|
|
199
|
+
parts = input_str.split()
|
|
200
|
+
|
|
201
|
+
if not parts:
|
|
202
|
+
return ""
|
|
203
|
+
|
|
204
|
+
command = parts[0].lower()
|
|
205
|
+
args = parts[1:]
|
|
206
|
+
|
|
207
|
+
# Проверяем, это команда или переменная
|
|
208
|
+
if command in self._commands:
|
|
209
|
+
result = self._commands[command].execute(args)
|
|
210
|
+
self._output(f"> {input_str}")
|
|
211
|
+
if result:
|
|
212
|
+
self._output(result)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
elif command in self._variables:
|
|
216
|
+
if args:
|
|
217
|
+
# Установка значения
|
|
218
|
+
self.set_variable(command, args[0])
|
|
219
|
+
else:
|
|
220
|
+
# Получение значения
|
|
221
|
+
value = self.get_variable(command)
|
|
222
|
+
self._output(f"{command} = {value}")
|
|
223
|
+
return str(self.get_variable(command))
|
|
224
|
+
|
|
225
|
+
else:
|
|
226
|
+
msg = f"Unknown command: {command}. Type 'help' for available commands."
|
|
227
|
+
self._output(f"> {input_str}")
|
|
228
|
+
self._output(msg)
|
|
229
|
+
return msg
|
|
230
|
+
|
|
231
|
+
def _output(self, text: str):
|
|
232
|
+
"""Добавляет строку в вывод"""
|
|
233
|
+
self._output_lines.append(text)
|
|
234
|
+
if len(self._output_lines) > self._max_output_lines:
|
|
235
|
+
self._output_lines.pop(0)
|
|
236
|
+
|
|
237
|
+
def get_output(self) -> List[str]:
|
|
238
|
+
"""Возвращает вывод консоли"""
|
|
239
|
+
return self._output_lines.copy()
|
|
240
|
+
|
|
241
|
+
def get_history(self) -> List[str]:
|
|
242
|
+
"""Возвращает историю команд"""
|
|
243
|
+
return self._history.copy()
|
|
244
|
+
|
|
245
|
+
# ---- ВСТРОЕННЫЕ КОМАНДЫ ----
|
|
246
|
+
|
|
247
|
+
def _cmd_help(self, *args) -> str:
|
|
248
|
+
"""Показывает справку"""
|
|
249
|
+
if args:
|
|
250
|
+
cmd_name = args[0].lower()
|
|
251
|
+
if cmd_name in self._commands:
|
|
252
|
+
cmd = self._commands[cmd_name]
|
|
253
|
+
return f"{cmd.name}: {cmd.description}\nUsage: {cmd.usage}"
|
|
254
|
+
else:
|
|
255
|
+
return f"Unknown command: {cmd_name}"
|
|
256
|
+
|
|
257
|
+
categories = {}
|
|
258
|
+
for cmd in self._commands.values():
|
|
259
|
+
if cmd.category not in categories:
|
|
260
|
+
categories[cmd.category] = []
|
|
261
|
+
categories[cmd.category].append(cmd)
|
|
262
|
+
|
|
263
|
+
output = ["Available commands:\n"]
|
|
264
|
+
for category, cmds in sorted(categories.items()):
|
|
265
|
+
output.append(f"[{category}]")
|
|
266
|
+
for cmd in cmds:
|
|
267
|
+
output.append(f" {cmd.name:<20} {cmd.description}")
|
|
268
|
+
|
|
269
|
+
return "\n".join(output)
|
|
270
|
+
|
|
271
|
+
def _cmd_list(self, *args) -> str:
|
|
272
|
+
"""Список всех команд и переменных"""
|
|
273
|
+
output = ["Commands:"]
|
|
274
|
+
for cmd in sorted(self._commands.values(), key=lambda c: c.name):
|
|
275
|
+
output.append(f" {cmd.name:<20} {cmd.description}")
|
|
276
|
+
|
|
277
|
+
output.append("\nVariables:")
|
|
278
|
+
for name, var in sorted(self._variables.items()):
|
|
279
|
+
output.append(f" {name:<20} = {var.value} ({var.description})")
|
|
280
|
+
|
|
281
|
+
return "\n".join(output)
|
|
282
|
+
|
|
283
|
+
def _cmd_clear(self, *args) -> str:
|
|
284
|
+
"""Очищает консоль"""
|
|
285
|
+
self._output_lines.clear()
|
|
286
|
+
return ""
|
|
287
|
+
|
|
288
|
+
def _cmd_echo(self, *args) -> str:
|
|
289
|
+
"""Выводит сообщение"""
|
|
290
|
+
return " ".join(args)
|
|
291
|
+
|
|
292
|
+
def _cmd_quit(self, *args) -> str:
|
|
293
|
+
"""Выход из движка"""
|
|
294
|
+
# Здесь должен быть вызов engine.shutdown()
|
|
295
|
+
return "Quitting..."
|
|
296
|
+
|
|
297
|
+
def _cmd_fps(self, *args) -> str:
|
|
298
|
+
"""Показывает FPS"""
|
|
299
|
+
from aether.core.engine import Engine
|
|
300
|
+
engine = Engine()
|
|
301
|
+
return f"FPS: {engine.time.fps:.1f}"
|
|
302
|
+
|
|
303
|
+
def _cmd_time_scale(self, *args) -> str:
|
|
304
|
+
"""Устанавливает масштаб времени"""
|
|
305
|
+
if args:
|
|
306
|
+
try:
|
|
307
|
+
scale = float(args[0])
|
|
308
|
+
self.set_variable("g_time_scale", scale)
|
|
309
|
+
return f"Time scale set to {scale}"
|
|
310
|
+
except ValueError:
|
|
311
|
+
return "Invalid time scale value"
|
|
312
|
+
return f"Time scale: {self.get_variable('g_time_scale')}"
|
|
313
|
+
|
|
314
|
+
def toggle(self):
|
|
315
|
+
"""Переключает видимость консоли"""
|
|
316
|
+
self.visible = not self.visible
|
|
317
|
+
if self.visible:
|
|
318
|
+
self._input_buffer = ""
|
|
319
|
+
|
|
320
|
+
def handle_input(self, char: str):
|
|
321
|
+
"""Обрабатывает ввод символа"""
|
|
322
|
+
if char == '\n': # Enter
|
|
323
|
+
self.execute(self._input_buffer)
|
|
324
|
+
self._input_buffer = ""
|
|
325
|
+
elif char == '\b': # Backspace
|
|
326
|
+
self._input_buffer = self._input_buffer[:-1]
|
|
327
|
+
elif char == '\t': # Tab (автодополнение)
|
|
328
|
+
self._autocomplete()
|
|
329
|
+
else:
|
|
330
|
+
self._input_buffer += char
|
|
331
|
+
|
|
332
|
+
def _autocomplete(self):
|
|
333
|
+
"""Автодополнение команд"""
|
|
334
|
+
if not self._input_buffer:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
matches = []
|
|
338
|
+
|
|
339
|
+
for cmd_name in self._commands:
|
|
340
|
+
if cmd_name.startswith(self._input_buffer.lower()):
|
|
341
|
+
matches.append(cmd_name)
|
|
342
|
+
|
|
343
|
+
for var_name in self._variables:
|
|
344
|
+
if var_name.startswith(self._input_buffer.lower()):
|
|
345
|
+
matches.append(var_name)
|
|
346
|
+
|
|
347
|
+
if len(matches) == 1:
|
|
348
|
+
self._input_buffer = matches[0]
|
|
349
|
+
elif len(matches) > 1:
|
|
350
|
+
# Показываем варианты
|
|
351
|
+
common_prefix = self._input_buffer
|
|
352
|
+
for i in range(len(self._input_buffer), len(matches[0])):
|
|
353
|
+
char = matches[0][i]
|
|
354
|
+
if all(m.startswith(common_prefix + char) for m in matches):
|
|
355
|
+
common_prefix += char
|
|
356
|
+
else:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
self._input_buffer = common_prefix
|
|
360
|
+
|
|
361
|
+
if len(matches) <= 10:
|
|
362
|
+
self._output(" ".join(matches))
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def input_text(self) -> str:
|
|
366
|
+
return self._input_buffer
|
aether/core/engine.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
def run(self):
|
|
2
|
+
"""Главный игровой цикл"""
|
|
3
|
+
self._running = True
|
|
4
|
+
print("[Aether] Starting game loop...")
|
|
5
|
+
|
|
6
|
+
from aether.utils.profiler import Profiler, ScopedProfiler
|
|
7
|
+
profiler = Profiler()
|
|
8
|
+
|
|
9
|
+
while self._running and not self.window.should_close():
|
|
10
|
+
profiler.begin_frame()
|
|
11
|
+
|
|
12
|
+
with ScopedProfiler("Input"):
|
|
13
|
+
self.time.update()
|
|
14
|
+
dt = min(self.time.delta_time, 0.1)
|
|
15
|
+
self.input.update()
|
|
16
|
+
|
|
17
|
+
if self.input.get_key(glfw.KEY_ESCAPE):
|
|
18
|
+
self.event_bus.emit('engine_quit')
|
|
19
|
+
|
|
20
|
+
if self.input.get_key_down(glfw.KEY_P):
|
|
21
|
+
self.event_bus.emit('engine_pause', paused=not self._paused)
|
|
22
|
+
|
|
23
|
+
# Профилировщик по F3
|
|
24
|
+
if self.input.get_key_down(glfw.KEY_F3):
|
|
25
|
+
profiler.print_report()
|
|
26
|
+
|
|
27
|
+
if not self._paused:
|
|
28
|
+
with ScopedProfiler("Physics"):
|
|
29
|
+
self._update_physics(dt)
|
|
30
|
+
|
|
31
|
+
with ScopedProfiler("Scene Update"):
|
|
32
|
+
if self.scene:
|
|
33
|
+
self.scene.update(dt)
|
|
34
|
+
|
|
35
|
+
self._on_update(dt)
|
|
36
|
+
|
|
37
|
+
with ScopedProfiler("Render"):
|
|
38
|
+
self._render()
|
|
39
|
+
|
|
40
|
+
with ScopedProfiler("Window"):
|
|
41
|
+
self.window.swap_buffers()
|
|
42
|
+
self.window.poll_events()
|
|
43
|
+
|
|
44
|
+
if self._frame_count % 60 == 0:
|
|
45
|
+
self._update_title()
|
|
46
|
+
|
|
47
|
+
profiler.end_frame()
|
|
48
|
+
self._frame_count += 1
|
|
49
|
+
|
|
50
|
+
self.shutdown()
|
aether/core/event_bus.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Система событий (Observer pattern)"""
|
|
2
|
+
from typing import Callable, Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EventBus:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self._listeners: Dict[str, List[Callable]] = {}
|
|
8
|
+
|
|
9
|
+
def subscribe(self, event_type: str, callback: Callable):
|
|
10
|
+
if event_type not in self._listeners:
|
|
11
|
+
self._listeners[event_type] = []
|
|
12
|
+
self._listeners[event_type].append(callback)
|
|
13
|
+
|
|
14
|
+
def unsubscribe(self, event_type: str, callback: Callable):
|
|
15
|
+
if event_type in self._listeners:
|
|
16
|
+
self._listeners[event_type].remove(callback)
|
|
17
|
+
|
|
18
|
+
def emit(self, event_type: str, **data):
|
|
19
|
+
if event_type in self._listeners:
|
|
20
|
+
for callback in self._listeners[event_type]:
|
|
21
|
+
callback(data)
|
|
22
|
+
|
|
23
|
+
def clear(self):
|
|
24
|
+
self._listeners.clear()
|
aether/core/input.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Обработка ввода: клавиатура, мышь, геймпад"""
|
|
2
|
+
import glfw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Input:
|
|
6
|
+
def __init__(self, window):
|
|
7
|
+
self.window = window
|
|
8
|
+
self._keys = {}
|
|
9
|
+
self._keys_prev = {}
|
|
10
|
+
self._mouse_buttons = {}
|
|
11
|
+
self._mouse_buttons_prev = {}
|
|
12
|
+
self.mouse_x = 0.0
|
|
13
|
+
self.mouse_y = 0.0
|
|
14
|
+
self.mouse_dx = 0.0
|
|
15
|
+
self.mouse_dy = 0.0
|
|
16
|
+
self._first_mouse = True
|
|
17
|
+
|
|
18
|
+
# Коллбэки
|
|
19
|
+
glfw.set_key_callback(window.handle, self._key_callback)
|
|
20
|
+
glfw.set_mouse_button_callback(window.handle, self._mouse_button_callback)
|
|
21
|
+
glfw.set_cursor_pos_callback(window.handle, self._cursor_callback)
|
|
22
|
+
|
|
23
|
+
def _key_callback(self, window, key, scancode, action, mods):
|
|
24
|
+
self._keys[key] = (action == glfw.PRESS or action == glfw.REPEAT)
|
|
25
|
+
|
|
26
|
+
def _mouse_button_callback(self, window, button, action, mods):
|
|
27
|
+
self._mouse_buttons[button] = (action == glfw.PRESS)
|
|
28
|
+
|
|
29
|
+
def _cursor_callback(self, window, xpos, ypos):
|
|
30
|
+
if self._first_mouse:
|
|
31
|
+
self.mouse_x = xpos
|
|
32
|
+
self.mouse_y = ypos
|
|
33
|
+
self._first_mouse = False
|
|
34
|
+
self.mouse_dx = xpos - self.mouse_x
|
|
35
|
+
self.mouse_dy = ypos - self.mouse_y
|
|
36
|
+
self.mouse_x = xpos
|
|
37
|
+
self.mouse_y = ypos
|
|
38
|
+
|
|
39
|
+
def update(self):
|
|
40
|
+
self._keys_prev = self._keys.copy()
|
|
41
|
+
self._mouse_buttons_prev = self._mouse_buttons.copy()
|
|
42
|
+
self.mouse_dx = 0.0
|
|
43
|
+
self.mouse_dy = 0.0
|
|
44
|
+
|
|
45
|
+
def get_key(self, key) -> bool:
|
|
46
|
+
return self._keys.get(key, False)
|
|
47
|
+
|
|
48
|
+
def get_key_down(self, key) -> bool:
|
|
49
|
+
return self._keys.get(key, False) and not self._keys_prev.get(key, False)
|
|
50
|
+
|
|
51
|
+
def get_key_up(self, key) -> bool:
|
|
52
|
+
return not self._keys.get(key, False) and self._keys_prev.get(key, False)
|
|
53
|
+
|
|
54
|
+
def get_mouse_button(self, button) -> bool:
|
|
55
|
+
return self._mouse_buttons.get(button, False)
|
|
56
|
+
|
|
57
|
+
def get_mouse_button_down(self, button) -> bool:
|
|
58
|
+
return self._mouse_buttons.get(button, False) and not self._mouse_buttons_prev.get(button, False)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Система плагинов — расширение движка без правки ядра"""
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import List, Type, Dict
|
|
4
|
+
import importlib
|
|
5
|
+
import os
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Plugin(ABC):
|
|
10
|
+
"""Базовый класс для всех плагинов"""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def on_register(self, engine):
|
|
14
|
+
"""Вызывается при регистрации плагина"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def on_unregister(self, engine):
|
|
19
|
+
"""Вызывается при удалении плагина"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
"""Уникальное имя плагина"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def version(self) -> str:
|
|
31
|
+
"""Версия плагина"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PluginManager:
|
|
36
|
+
"""Менеджер плагинов"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self._plugins: Dict[str, Plugin] = {}
|
|
40
|
+
self._plugin_dirs: List[str] = ["./plugins"]
|
|
41
|
+
|
|
42
|
+
def add_plugin_dir(self, path: str):
|
|
43
|
+
"""Добавляет директорию для поиска плагинов"""
|
|
44
|
+
self._plugin_dirs.append(path)
|
|
45
|
+
|
|
46
|
+
def discover_plugins(self):
|
|
47
|
+
"""Автоматически находит плагины в указанных директориях"""
|
|
48
|
+
for plugin_dir in self._plugin_dirs:
|
|
49
|
+
if not os.path.exists(plugin_dir):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
for filename in os.listdir(plugin_dir):
|
|
53
|
+
if filename.endswith('.py') and not filename.startswith('_'):
|
|
54
|
+
module_name = filename[:-3]
|
|
55
|
+
spec = importlib.util.spec_from_file_location(
|
|
56
|
+
module_name,
|
|
57
|
+
os.path.join(plugin_dir, filename)
|
|
58
|
+
)
|
|
59
|
+
module = importlib.util.module_from_spec(spec)
|
|
60
|
+
spec.loader.exec_module(module)
|
|
61
|
+
|
|
62
|
+
for name, obj in inspect.getmembers(module):
|
|
63
|
+
if (inspect.isclass(obj) and
|
|
64
|
+
issubclass(obj, Plugin) and
|
|
65
|
+
obj != Plugin):
|
|
66
|
+
self.register_plugin(obj())
|
|
67
|
+
|
|
68
|
+
def register_plugin(self, plugin: Plugin, engine=None):
|
|
69
|
+
"""Регистрирует плагин"""
|
|
70
|
+
if plugin.name in self._plugins:
|
|
71
|
+
raise ValueError(f"Plugin {plugin.name} already registered!")
|
|
72
|
+
|
|
73
|
+
self._plugins[plugin.name] = plugin
|
|
74
|
+
if engine:
|
|
75
|
+
plugin.on_register(engine)
|
|
76
|
+
print(f"[Plugin] Registered: {plugin.name} v{plugin.version}")
|
|
77
|
+
|
|
78
|
+
def unregister_plugin(self, name: str, engine=None):
|
|
79
|
+
"""Удаляет плагин"""
|
|
80
|
+
if name in self._plugins:
|
|
81
|
+
plugin = self._plugins[name]
|
|
82
|
+
if engine:
|
|
83
|
+
plugin.on_unregister(engine)
|
|
84
|
+
del self._plugins[name]
|
|
85
|
+
|
|
86
|
+
def get_plugin(self, name: str) -> Plugin:
|
|
87
|
+
"""Получает плагин по имени"""
|
|
88
|
+
return self._plugins.get(name)
|
|
89
|
+
|
|
90
|
+
def initialize_all(self, engine):
|
|
91
|
+
"""Инициализирует все плагины"""
|
|
92
|
+
for plugin in self._plugins.values():
|
|
93
|
+
plugin.on_register(engine)
|