]> Repositories - rushbound.git/blob - addons/godot_super-wakatime/godot_super-wakatime.gd
Install Godot Super Wakatime
[rushbound.git] / addons / godot_super-wakatime / godot_super-wakatime.gd
1 @tool
2 extends EditorPlugin
3
4
5 #------------------------------- SETUP -------------------------------
6 # Utilities
7 var Utils = preload("res://addons/godot_super-wakatime/utils.gd").new()
8 var DecompressorUtils = preload("res://addons/godot_super-wakatime/decompressor.gd").new()
9
10 # Hearbeat class
11 const HeartBeat = preload("res://addons/godot_super-wakatime/heartbeat.gd")
12 var last_heartbeat = HeartBeat.new()
13
14 # Paths, Urls
15 const PLUGIN_PATH: String = "res://addons/godot_super-wakatime"
16 const ZIP_PATH: String = "%s/wakatime.zip" % PLUGIN_PATH
17
18 const WAKATIME_URL_FMT: String = \
19         "https://github.com/wakatime/wakatime-cli/releases/download/v1.54.0/{wakatime_build}.zip"
20 const DECOMPERSSOR_URL_FMT: String = \
21         "https://github.com/ouch-org/ouch/releases/download/0.3.1/{ouch_build}"
22
23 # Names for menu
24 const API_MENU_ITEM: String = "Wakatime API key"
25 const CONFIG_MENU_ITEM: String = "Wakatime Config File"
26
27 # Directories to grab wakatime from
28 var wakatime_dir = null
29 var wakatime_cli = null
30 var decompressor_cli = null
31
32 var ApiKeyPrompt: PackedScene = preload("res://addons/godot_super-wakatime/api_key_prompt.tscn")
33 var Counter: PackedScene = preload("res://addons/godot_super-wakatime/counter.tscn")
34
35 # Set platform
36 var system_platform: String = Utils.set_platform()[0]
37 var system_architecture: String = Utils.set_platform()[1]
38
39 var debug: bool = false
40 var last_scene_path: String = ''
41 var last_file_path: String = ''
42 var last_time: int = 0
43 var previous_state: String = ''
44 const LOG_INTERVAL: int = 60000
45 var scene_mode: bool = false
46
47 var key_get_tries: int = 0
48 var counter_instance: Node
49 var current_time: String = "0 hrs, 0mins"
50
51
52 # #------------------------------- DIRECT PLUGIN FUNCTIONS -------------------------------
53 func _ready() -> void:
54         setup_plugin()
55         set_process(true)
56         
57 func _exit_tree() -> void:
58         _disable_plugin()
59         set_process(false)
60         
61         
62 func _physics_process(delta: float) -> void:
63         """Process plugin changes over time"""
64         # Every 1000 frames check for updates
65         if Engine.get_physics_frames() % 1000 == 0:
66                 # Check for scene change
67                 var scene_root = get_editor_interface().get_edited_scene_root()
68                 if scene_root:
69                         var current_scene_path = _get_current_scene()
70                         
71                         # If currently used scene is different thatn 1000 frames ago, log activity
72                         if current_scene_path != last_scene_path:
73                                 last_scene_path = current_scene_path
74                                 handle_activity_scene(current_scene_path, false, true)
75                                 
76                         else:
77                                 # Check for scene updates
78                                 var current_scene = EditorInterface.get_edited_scene_root()
79                                 if current_scene:
80                                         var state = generate_scene_state(current_scene)
81                                         # If current state is different than the previous one, handle activity
82                                         if state != previous_state:
83                                                 previous_state = state
84                                                 handle_activity_scene(current_scene_path, true)
85                                         # Otherwise just keep scene the same
86                                         else:
87                                                 previous_state = state
88                 else:
89                         last_scene_path = '' 
90                                         
91 func generate_scene_state(node: Node) -> String:
92         """Generate a scene state identifier"""
93         var state = str(node.get_instance_id())
94         for child in node.get_children():
95                 state += str(child.get_instance_id())
96         return str(state)
97         
98 func _input(event: InputEvent) -> void:
99         """Handle all input events"""
100         # Key events
101         if event is InputEventKey:
102                 var file = get_current_file()
103                 if file:
104                         handle_activity(ProjectSettings.globalize_path(file.resource_path))
105         # Mouse button events
106         elif event is InputEventMouse and event.is_pressed():
107                 var file = _get_current_scene()
108                 if file != '' and file:
109                         handle_activity_scene(file)
110
111 func setup_plugin() -> void:
112         """Setup Wakatime plugin, download dependencies if needed, initialize menu"""
113         Utils.plugin_print("Setting up %s" % get_user_agent())
114         check_dependencies()
115         
116         # Grab API key if needed
117         var api_key = get_api_key()
118         if api_key == null:
119                 request_api_key()
120         await get_tree().process_frame
121         
122         # Add menu buttons
123         add_tool_menu_item(API_MENU_ITEM, request_api_key)
124         add_tool_menu_item(CONFIG_MENU_ITEM, open_config)
125         
126         counter_instance = Counter.instantiate()
127         add_control_to_bottom_panel(counter_instance, current_time)
128         
129         # Connect code editor signals
130         var script_editor: ScriptEditor = get_editor_interface().get_script_editor()
131         script_editor.call_deferred("connect", "editor_script_changed", Callable(self, 
132                 "_on_script_changed"))
133
134 func _disable_plugin() -> void:
135         """Cleanup after disabling plugin"""
136         # Remove items from menu
137         remove_tool_menu_item(API_MENU_ITEM)
138         remove_tool_menu_item(CONFIG_MENU_ITEM)
139         
140         remove_control_from_bottom_panel(counter_instance)
141         
142         # Disconnect script editor tracking
143         var script_editor: ScriptEditor = get_editor_interface().get_script_editor()
144         if script_editor.is_connected("editor_script_changed", Callable(self, 
145                 "_on_script_changed")):
146                 script_editor.disconnect("editor_script_changed", Callable(self, 
147                         "_on_script_changed"))
148                 
149 func _on_script_changed(file) -> void:
150         """Handle changing scripts"""
151         if file:
152                 last_file_path = ProjectSettings.globalize_path(file.resource_path)
153         handle_activity(last_file_path)
154         
155 #func _unhandled_key_input(event: InputEvent) -> void:
156 #       """Handle key inputs"""
157 #       var file = get_current_file()
158 #       handle_activity(file)
159         
160 func _save_external_data() -> void:
161         """Handle saving files"""
162         var file = get_current_file()
163         if file:
164                 handle_activity(ProjectSettings.globalize_path(file.resource_path), true)
165         
166 func _get_current_scene():
167         """Get currently used scene"""
168         if EditorInterface.get_edited_scene_root():
169                 var file = EditorInterface.get_edited_scene_root()
170                 if file:
171                         return ProjectSettings.globalize_path(file.scene_file_path)
172         else:
173                 var file = get_current_file()
174                 if file:
175                         return ProjectSettings.globalize_path(file.resource_path)
176                         
177         return null
178         
179 func _on_scene_modified():
180         """Send heartbeat when scene is modified"""
181         var current_scene = get_tree().current_scene
182         if current_scene:
183                 handle_activity_scene(_get_current_scene())
184         
185 func get_current_file():
186         """Get currently used script file"""
187         var file = get_editor_interface().get_script_editor().get_current_script()
188         if file:
189                 last_file_path = ProjectSettings.globalize_path(file.resource_path)
190         
191         return get_editor_interface().get_script_editor().get_current_script()
192                 
193 func handle_activity(file, is_write: bool = false) -> void:
194         """Handle user's activity"""
195         # If file that has activity in or wakatime cli doesn't exist, return
196         if not file or not Utils.wakatime_cli_exists(get_waka_cli()):
197                 return
198         
199         # If user is saving file or has changed path, or enough time has passed for a heartbeat - send it
200         #var filepath = ProjectSettings.globalize_path(file.resource_path)
201         if is_write or file != last_heartbeat.file_path or enough_time_passed():
202                 send_heartbeat(file, is_write)
203                 
204 func handle_activity_scene(file, is_write: bool = false, changed_file: bool = false) -> void:
205         """Handle activity in scenes"""
206         if not file or not Utils.wakatime_cli_exists(get_waka_cli()):
207                 return
208                 
209         if is_write or changed_file or enough_time_passed():
210                 scene_mode = true
211                 send_heartbeat(file, is_write)
212                 
213 func send_heartbeat(filepath: String, is_write: bool) -> void:
214         """Send Wakatimde heartbeat for the specified file"""
215         # Check Wakatime API key
216         var api_key = get_api_key()
217         if api_key == null:
218                 Utils.plugin_print("Failed to get Wakatime API key. Are you sure it's correct?")
219                 if (key_get_tries < 3):
220                         request_api_key()
221                         key_get_tries += 1
222                 else:
223                         Utils.plugin_print("If this keep occuring, please create a file: ~/.wakatime.cfg\n
224                                 initialize it with:\n[settings]\napi_key={your_key}")
225                 return
226                 
227         # Make sure not to trigger additional heartbeats cause of events from scenes
228         var file = filepath
229         if scene_mode:
230                 file = last_file_path
231                 #print("\n-------SCENE MODE--------\n")
232                 
233         # Create heartbeat
234         var heartbeat = HeartBeat.new(file, Time.get_unix_time_from_system(), is_write)
235         
236         # Current text editor
237         var text_editor = _find_active_script_editor()
238         var cursor_pos = _get_cursor_pos(text_editor)
239         
240         # Append all informations as Wakatime CLI arguments
241         var cmd: Array[Variant] = ["--entity", filepath, "--key", api_key]
242         if is_write:
243                 cmd.append("--write")
244         cmd.append_array(["--alternate-project", ProjectSettings.get("application/config/name")])
245         cmd.append_array(["--time", str(heartbeat.time)])
246         cmd.append_array(["--lineno", str(cursor_pos.line)])
247         cmd.append_array(["--cursorpos", str(cursor_pos.column)])
248         cmd.append_array(["--plugin", get_user_agent()])
249         
250         cmd.append_array(["--alternate-language", "Scene"])
251         if scene_mode:
252                 cmd.append_array(["--category", "building"])
253         else:
254                 cmd.append(["--category", "coding"])    
255         
256         # Send heartbeat using Wakatime CLI
257         var cmd_callable = Callable(self, "_handle_heartbeat").bind(cmd)
258         
259         scene_mode = false
260         
261         WorkerThreadPool.add_task(cmd_callable)
262         last_heartbeat = heartbeat
263         
264 func _find_active_script_editor():
265         """Return currently used script editor"""
266         # Get script editor
267         var script_editor = get_editor_interface().get_script_editor()
268         var current_editor = script_editor.get_current_editor()
269         
270         # Try to find code editor from it
271         if current_editor:
272                 return _find_code_edit_recursive(script_editor.get_current_editor())
273         return null
274
275 func _find_code_edit_recursive(node: Node) -> CodeEdit:
276         """Find recursively code editor in a node"""
277         # If node is already a code editor, return it
278         if node is CodeEdit:
279                 return node
280
281         # Try to find it in every child of a given node         
282         for child in node.get_children():
283                 var editor = _find_code_edit_recursive(child)
284                 if editor:
285                         return editor
286         return null
287         
288 func _get_cursor_pos(text_editor) -> Dictionary:
289         """Get cursor editor from the given text editor"""
290         if text_editor:
291                 
292                 return {
293                         "line": text_editor.get_caret_line() + 1,
294                         "column": text_editor.get_caret_column() + 1
295                 }
296                 
297         return {
298                 "line": 0,
299                 "column": 0
300         }
301         
302 func _handle_heartbeat(cmd_arguments) -> void:
303         """Handle sending the heartbeat"""
304         # Get Wakatime CLI and try to send a heartbeat
305         if wakatime_cli == null:
306                 wakatime_cli = Utils.get_waka_cli()
307                 
308         var output: Array[Variant] = []
309         var exit_code: int = OS.execute(wakatime_cli, cmd_arguments, output, true)
310         
311         update_today_time(wakatime_cli)
312                 
313         # Inform about success or errors if user is in debug
314         if debug:
315                 if exit_code == -1:
316                         Utils.plugin_print("Failed to send heartbeat: %s" % output)
317                 else:
318                         Utils.plugin_print("Heartbeat sent: %s" % output)
319                         
320 func enough_time_passed():
321         """Check if enough time has passed for another heartbeat"""
322         return Time.get_unix_time_from_system() - last_heartbeat.time >= HeartBeat.FILE_MODIFIED_DELAY
323         
324 func update_today_time(wakatime_cli) -> void:
325         """Update today's time in menu"""
326         var output: Array[Variant] = []
327         # Get today's time from Wakatime CLI
328         var exit_code: int = OS.execute(wakatime_cli, ["--today"], output, true)
329         
330         # Convert it and combine different categories into
331         if exit_code == 0:
332                 current_time = convert_time(output[0])
333         else:
334                 current_time = "Wakatime"
335         #print(current_time)
336         call_deferred("_update_panel_label", current_time, output[0])
337         
338 func _update_panel_label(label: String, content: String):
339         """Update bottom panel name that shows time"""
340         # If counter exists and it has a label, update both the label and panel's name
341         if counter_instance and counter_instance.get_node("HBoxContainer/Label"):
342                 counter_instance.get_node("HBoxContainer/Label").text = content
343                 # Workaround to rename panel
344                 remove_control_from_bottom_panel(counter_instance)
345                 add_control_to_bottom_panel(counter_instance, label)
346                 
347 func convert_time(complex_time: String):
348         """Convert time from complex format into basic one, combine times"""
349         return complex_time
350         
351         """
352         # Split times into categories
353         var time_categories = complex_time.split(', ')
354         for category in time_categories:
355                 # Split time into parts, get first and third part (hours and minutes)
356                 var time_parts = category.split(' ')
357                 if time_parts.size() >= 3:
358                         hours += int(time_parts[0])
359                         minutes += int(time_parts[2])
360         
361         # Wrap minutes into hours if needed
362         while minutes >= 60:
363                 minutes -= 60
364                 hours += 1
365
366         return str(hours) + " hrs, " + str(minutes) + " mins"
367         """
368
369 #------------------------------- FILE FUNCTIONS -------------------------------
370 func open_config() -> void:
371         """Open wakatime config file"""
372         OS.shell_open(Utils.config_filepath(system_platform, PLUGIN_PATH))
373         
374 func get_waka_dir() -> String:
375         """Search for and return wakatime directory"""
376         if wakatime_dir == null:
377                 wakatime_dir = "%s/.wakatime" % Utils.home_directory(system_platform, PLUGIN_PATH)
378         return wakatime_dir
379         
380 func get_waka_cli() -> String:
381         """Get wakatime_cli file"""
382         if wakatime_cli == null:
383                 var build = Utils.get_waka_build(system_platform, system_architecture)
384                 var ext: String = ".exe" if system_platform == "windows" else ''
385                 wakatime_cli = "%s/%s%s" % [get_waka_dir(), build, ext]
386         return wakatime_cli
387         
388 func check_dependencies() -> void:
389         """Make sure all dependencies exist"""
390         if !Utils.wakatime_cli_exists(get_waka_cli()):
391                 download_wakatime()
392                 if !DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
393                         download_decompressor()
394         
395 func download_wakatime() -> void:
396         """Download wakatime cli"""
397         Utils.plugin_print("Downloading Wakatime CLI...")
398         var url: String = WAKATIME_URL_FMT.format({"wakatime_build": 
399                         Utils.get_waka_build(system_platform, system_architecture)})
400         
401         # Try downloading wakatime
402         var http = HTTPRequest.new()
403         http.download_file = ZIP_PATH
404         http.connect("request_completed", Callable(self, "_wakatime_download_completed"))
405         add_child(http)
406         
407         # Handle errors
408         var status = http.request(url)
409         if status != OK:
410                 Utils.plugin_print_err("Failed to start downloading Wakatime [Error: %s]" % status)
411                 _disable_plugin()
412         
413 func download_decompressor() -> void:
414         """Download ouch decompressor"""
415         Utils.plugin_print("Downloading Ouch! decompression library...")
416         var url: String = DECOMPERSSOR_URL_FMT.format({"ouch_build": 
417                         Utils.get_ouch_build(system_platform)})
418         if system_platform == "windows":
419                 url += ".exe"
420                 
421         # Try to download ouch
422         var http = HTTPRequest.new()
423         http.download_file = DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH)
424         http.connect("request_completed", Callable(self, "_decompressor_download_finished"))
425         add_child(http)
426         
427         # Handle errors
428         var status = http.request(url)
429         if status != OK:
430                 _disable_plugin()
431                 Utils.plugin_print_err("Failed to start downloading Ouch! library [Error: %s]" % status)
432                 
433 func _wakatime_download_completed(result, status, headers, body) -> void:
434         """Finish downloading wakatime, handle errors"""
435         if result != HTTPRequest.RESULT_SUCCESS:
436                 Utils.plugin_print_err("Error while downloading Wakatime CLI")
437                 _disable_plugin()
438                 return
439         
440         Utils.plugin_print("Wakatime CLI has been installed succesfully! Located at %s" % ZIP_PATH)
441         extract_files(ZIP_PATH, get_waka_dir())
442         
443 func _decompressor_download_finished(result, status, headers, body) -> void:
444         """Handle errors and finishi decompressor download"""
445         # Error while downloading
446         if result != HTTPRequest.RESULT_SUCCESS:
447                 Utils.plugin_print_err("Error while downloading Ouch! library")
448                 _disable_plugin()
449                 return
450         
451         # Error while saving
452         if !DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
453                 Utils.plugin_print_err("Failed to save Ouch! library")
454                 _disable_plugin()
455                 return
456
457         # Save decompressor path, give write permissions to it
458         var decompressor: String = \
459                 ProjectSettings.globalize_path(DecompressorUtils.decompressor_cli(decompressor_cli, 
460                         system_platform, PLUGIN_PATH))
461                         
462         if system_platform == "linux" or system_platform == "darwin":
463                 OS.execute("chmod", ["+x", decompressor], [], true)
464                 
465         # Extract files, allowing usage of Ouch!
466         Utils.plugin_print("Ouch! has been installed succesfully! Located at %s" % \
467                 DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
468         extract_files(ZIP_PATH, get_waka_dir())
469         
470 func extract_files(source: String, destination: String) -> void:
471         """Extract downloaded Wakatime zip"""
472         # If decompression library and wakatime zip folder don't exist, return
473         if not DecompressorUtils.lib_exists(decompressor_cli, system_platform, 
474                         PLUGIN_PATH) and not Utils.wakatime_zip_exists(ZIP_PATH):
475                 return
476                 
477         # Get paths as global
478         Utils.plugin_print("Extracting Wakatime...")
479         var decompressor: String
480         if system_platform == "windows":
481                 decompressor = ProjectSettings.globalize_path( 
482                         DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
483         else:
484                 decompressor = ProjectSettings.globalize_path("res://" +
485                         DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
486                 
487         var src: String = ProjectSettings.globalize_path(source)
488         var dest: String = ProjectSettings.globalize_path(destination)
489         
490         # Execute Ouch! decompression command, catch errors
491         var errors: Array[Variant] = []
492         var args: Array[String] = ["--yes", "decompress", src, "--dir", dest]
493         
494         var error: int = OS.execute(decompressor, args, errors, true)
495         if error:
496                 Utils.plugin_print(errors)
497                 _disable_plugin()
498                 return
499                 
500         # Results
501         if Utils.wakatime_cli_exists(get_waka_cli()):
502                 Utils.plugin_print("Wakatime CLI installed (path: %s)" % get_waka_cli())
503         else:
504                 Utils.plugin_print_err("Installation of Wakatime failed")
505                 _disable_plugin()
506                 
507         # Remove leftover files
508         clean_files()
509         
510 func clean_files():
511         """Delete files that aren't needed anymore"""
512         if Utils.wakatime_zip_exists(ZIP_PATH):
513                 delete_file(ZIP_PATH)
514         if DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
515                 delete_file(ZIP_PATH)
516                 
517 func delete_file(path: String) -> void:
518         """Delete file at specified path"""
519         var dir: DirAccess = DirAccess.open("res://")
520         var status: int = dir.remove(path)
521         if status != OK:
522                 Utils.plugin_print_err("Failed to clean unnecesary file at %s" % path)
523         else:
524                 Utils.plugin_print("Clean unncecesary file at %s" % path)
525
526 #------------------------------- API KEY FUNCTIONS -------------------------------
527
528 func get_api_key():
529         """Get wakatime api key"""
530         var result = []
531         # Handle errors while getting the key
532         var err = OS.execute(get_waka_cli(), ["--config-read", "api_key"], result)
533         if err == -1:
534                 return null
535                 
536         # Trim API key from whitespaces and return it
537         var key = result[0].strip_edges()
538         if key.is_empty():
539                 return null
540         return key
541         
542 func request_api_key() -> void:
543         """Request Wakatime API key from the user"""
544         # Prepare prompt
545         var prompt = ApiKeyPrompt.instantiate()
546         _set_api_key(prompt, get_api_key())
547         _register_api_key_signals(prompt)
548         
549         # Show prompt and hide it on request
550         add_child(prompt)
551         prompt.popup_centered()
552         await prompt.popup_hide
553         prompt.queue_free()
554         
555 func _set_api_key(prompt: PopupPanel, api_key) -> void:
556         """Set API key from prompt"""
557         # Safeguard against empty key
558         if api_key == null:
559                 api_key = ''
560                 
561         # Set correct text, to show API key
562         var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
563         edit_field.text = api_key
564         
565 func _register_api_key_signals(prompt: PopupPanel) -> void:
566         """Connect all signals related to API key popup"""
567         # Get all Nodes
568         var show_button: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/ShowButton")
569         var save_button: Node = prompt.get_node("VBoxContainer/HBoxContainerBottom/SubmitButton")
570         
571         # Connect them to press events
572         show_button.connect("pressed", Callable(self, "_on_toggle_key_text").bind(prompt))
573         save_button.connect("pressed", Callable(self, "_on_save_key").bind(prompt))
574         prompt.connect("popup_hide", Callable(self, "_on_popup_hide").bind(prompt))
575         
576 func _on_popup_hide(prompt: PopupPanel):
577         """Close the popup window when user wants to hide it"""
578         prompt.queue_free()
579         
580 func _on_toggle_key_text(prompt: PopupPanel) -> void:
581         """Handle hiding and showing API key"""
582         # Get nodes
583         var show_button: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/ShowButton")
584         var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
585         
586         # Set the correct text and hide field if needed
587         edit_field.secret = not edit_field.secret
588         show_button.text = "Show" if edit_field.secret else "Hide"
589         
590 func _on_save_key(prompt: PopupPanel) -> void:
591         """Handle entering API key"""
592         # Get text field node and api key that's entered
593         var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
594         var api_key  = edit_field.text.strip_edges()
595         
596         # Try to set api key for wakatime and handle errors
597         var err: int = OS.execute(get_waka_cli(), ["--config-write", "api-key=%s" % api_key])
598         if err == -1:
599                 Utils.plugin_print("Failed to save API key")
600         prompt.visible = false
601         
602 #------------------------------- PLUGIN INFORMATIONS -------------------------------
603         
604 func get_user_agent() -> String:
605         """Get user agent identifier"""
606         var os_name = OS.get_name().to_lower()
607         return "godot/%s godot-wakatime/%s" % [
608                 get_engine_version(), 
609                 _get_plugin_version()
610         ]
611         
612 func _get_plugin_name() -> String:
613         """Get name of the plugin"""
614         return "Godot_Super-Wakatime"
615         
616 func _get_plugin_version() -> String:
617         """Get plugin version"""
618         return "1.0.0"
619         
620 func _get_editor_name() -> String:
621         """Get name of the editor"""
622         return "Godot%s" % get_engine_version()
623         
624 func get_engine_version() -> String:
625         """Get verison of currently used engine"""
626         return "%s.%s.%s" % [Engine.get_version_info()["major"], Engine.get_version_info()["minor"], 
627                 Engine.get_version_info()["patch"]]