├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── __init__.gd ├── docs ├── .nojekyll ├── README.md ├── _coverpage.md └── index.html ├── editor ├── __init__.gd └── tools.gd ├── filesystem ├── __init__.gd ├── path.gd └── shutil.gd ├── input ├── GestureDetector.gd └── __init__.gd ├── plugin.cfg ├── plugin.gd ├── resource ├── GridBackgroundTexture.gd ├── __init__.gd ├── image.gd └── resource.gd ├── scene ├── __init__.gd └── gui │ ├── AsyncHttpTextutreRect.gd │ ├── AsyncTextureRect.gd │ ├── GridBackground.gd │ ├── InfinityList.gd │ ├── ItemListEnhanced.gd │ ├── Menu.gd │ └── __init__.gd ├── setup.py └── utils ├── AsyncTaskQueue.gd ├── InstanceManager.gd ├── ObjectPool.gd ├── RandomPool.gd ├── TexturePacker.gd ├── __init__.gd ├── csv.gd ├── http.gd ├── json.gd ├── utils.gd └── uuid.gd /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.gd] 2 | indent_style = tab 3 | indent_size = 4 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = false 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ################################################################################## 2 | # Tool generated DO NOT modify # 3 | ################################################################################## 4 | # This file is part of # 5 | # GodotExplorer # 6 | # https://github.com/GodotExplorer # 7 | ################################################################################## 8 | # Copyright (c) 2017-2019 Godot Explorer # 9 | # # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy # 11 | # of this software and associated documentation files (the "Software"), to deal # 12 | # in the Software without restriction, including without limitation the rights # 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # 14 | # copies of the Software, and to permit persons to whom the Software is # 15 | # furnished to do so, subject to the following conditions: # 16 | # # 17 | # The above copyright notice and this permission notice shall be included in all # 18 | # copies or substantial portions of the Software. # 19 | # # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # 26 | # SOFTWARE. # 27 | ################################################################################## 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Utilities 2 | Utilities for godot game engine writing in GDScript 3 | 4 | # Quick Start 5 | 1. Copy the folder `gdutils` into `res://addons` 6 | 2. Activate the plugin `Godot Utilities` in your project manager -------------------------------------------------------------------------------- /__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | extends Node 4 | 5 | const editor = preload("editor/__init__.gd") 6 | const filesystem = preload("filesystem/__init__.gd") 7 | const input = preload("input/__init__.gd") 8 | const resource = preload("resource/__init__.gd") 9 | const scene = preload("scene/__init__.gd") 10 | const utils = preload("utils/__init__.gd") 11 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GodotExplorer/gdutils/62e96df7b71d8a9f7f6cd5c3de5db19916f56627/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 1. Copy the folder `gdutils` into `res://addons` 3 | 2. Activate the plugin `gdutils` in your project manager -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # Godot Utilities 2 | 3 | > Utilities for godot game engine writing in GDScript 4 | 5 | [GitHub](https://github.com/GodotExplorer/gdutils) 6 | [Get Started](/?id=quick-start) -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 |
Loading
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /editor/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const tools = preload("tools.gd") 5 | -------------------------------------------------------------------------------- /editor/tools.gd: -------------------------------------------------------------------------------- 1 | tool 2 | const MENUS = { 3 | "Open user://": 0, 4 | } 5 | 6 | func initialize(plugin): 7 | for key in MENUS: 8 | plugin.add_tool_menu_item(key, self, '_on_menu_pressed', MENUS[key]) 9 | 10 | func _on_menu_pressed(action): 11 | match action: 12 | 0: 13 | OS.shell_open(str('file://', OS.get_user_data_dir())) 14 | -------------------------------------------------------------------------------- /filesystem/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const path = preload("path.gd") 5 | const shutil = preload("shutil.gd") 6 | -------------------------------------------------------------------------------- /filesystem/path.gd: -------------------------------------------------------------------------------- 1 | # This module implements some useful functions on file path. 2 | 3 | tool 4 | 5 | # Return a list containing the names of the files in the directory. 6 | # It does not include the special entries '.' and '..' even if they are present in the directory. 7 | # - - - - - - - - - - 8 | # *Parameters* 9 | # * [path:String] The directory to search with 10 | # - - - - - - - - - - 11 | # *Returns* Array 12 | # * The list containing the names of the files in the directory 13 | # * Return `[]` if failed to search the target directory 14 | static func list_dir(path): 15 | var pathes = [] 16 | var dir = Directory.new() 17 | if dir.open(path) == OK: 18 | dir.list_dir_begin() 19 | var cwd = dir.get_next() 20 | while not cwd.empty(): 21 | if not cwd in ['.', '..']: 22 | pathes.append(cwd) 23 | cwd = dir.get_next() 24 | dir.list_dir_end() 25 | return pathes 26 | 27 | # Get file pathes in a list under target folder 28 | # - - - - - - - - - - 29 | # *Parameters* 30 | # * [path:String] The folder to search from 31 | # * [with_dirs:bool = false] Includes directories 32 | # * [recurse:String = false] Search sub-folders recursely 33 | # - - - - - - - - - - 34 | # *Returns* Array 35 | # * File pathes in an array 36 | static func list_files(path, with_dirs=false,recurse=false): 37 | var files = [] 38 | var dir = Directory.new() 39 | if dir.open(path) == OK: 40 | dir.list_dir_begin() 41 | var file_name = dir.get_next() 42 | while not file_name.empty(): 43 | if dir.current_is_dir() and not (file_name in [".", "..", "./"]): 44 | if recurse: 45 | var childfiles = list_files(str(path, "/", file_name), with_dirs, recurse) 46 | for f in childfiles: 47 | files.append(f) 48 | if not (file_name in [".", ".."]): 49 | var rpath = path 50 | if rpath.ends_with("/"): 51 | pass 52 | elif rpath == ".": 53 | rpath = "" 54 | else: 55 | rpath += "/" 56 | if not with_dirs and dir.current_is_dir(): 57 | pass 58 | else: 59 | rpath = str(rpath, file_name).replace("/./", "/") 60 | files.append(rpath) 61 | file_name = dir.get_next() 62 | return files 63 | 64 | # Join two or more path components intelligently. 65 | # The return value is the concatenation of path and any members of rest parameters 66 | # with exactly one directory separator `/` 67 | # *Parameters* 68 | # * [`p0~p9`:String] The paths 69 | # - - - - - - - - - - 70 | # *Returns* String 71 | # * The returen path is `normalized` 72 | static func join(p0, p1, p2='', p3='', p4='', p5='', p6='', p7='', p8='', p9=''): 73 | var args = [p0, p1, p2, p3, p4, p5, p6, p7, p8, p9] 74 | var path = "" 75 | var index = -1 76 | for p in args: 77 | index += 1 78 | p = normalize(p) 79 | if p.empty(): 80 | continue 81 | var sep = '/' 82 | if path.ends_with('/') or p.begins_with('/') or index <= 0: 83 | sep = '' 84 | path += str(sep, p) 85 | return path 86 | 87 | # Check is the path is under target directory 88 | # This just simple check with file path strings so it won't check the file or 89 | # directory in your file system 90 | # - - - - - - - - - - 91 | # *Parameters* 92 | # * [path:String] The file path 93 | # * [dir:String] The parent directory path 94 | # - - - - - - - - - - 95 | # *Returns* bool 96 | # * Is the file with `path` is under the directory ` dir` 97 | static func path_under_dir(path, dir): 98 | var d = normalize(dir) 99 | if not d.ends_with('/'): 100 | d += '/' 101 | var p = normalize(path) 102 | return p.begins_with(d) and p != d 103 | 104 | # Get sub-path from the parent directory 105 | # - - - - - - - - - - 106 | # *Parameters* 107 | # * [path:String] The file path 108 | # * [dir:String] The parent directory path 109 | # - - - - - - - - - - 110 | # *Returns* String 111 | # * The sub-path string 112 | static func relative_to_parent_dir(path, dir): 113 | var p = path 114 | if path_under_dir(p, dir): 115 | p = normalize(p) 116 | var d = normalize(dir) + '/' 117 | p = p.substr(d.length(), p.length()) 118 | return p 119 | 120 | # Normalize the file path this file replace all `\` and `//` to `/ ` 121 | # - - - - - - - - - - 122 | # *Parameters* 123 | # * [p_path:String] The file path to normalize to 124 | # - - - - - - - - - - 125 | # *Returns* String 126 | # * The formated string 127 | static func normalize(p_path): 128 | var replacement = { 129 | '\\': '/', 130 | '//': '/' 131 | } 132 | var path = p_path 133 | for key in replacement: 134 | path = path.replace(key, replacement[key]) 135 | return path 136 | -------------------------------------------------------------------------------- /filesystem/shutil.gd: -------------------------------------------------------------------------------- 1 | # The shutil module offers a number of high-level operations on files and collections of files. 2 | # In particular, functions are provided which support file copying and removal. 3 | 4 | tool 5 | 6 | # Copy the file `src` to the file `dst`. 7 | # Permission bits are ignored. `src` and `dst` are path names given as strings. 8 | # - - - 9 | # **Parameters** 10 | # * src: String The source file to copy from 11 | # * dst: String The destination file to copy 12 | # * buff_size: int The buffer size allocated while coping files 13 | # - - - 14 | # **Return** 15 | # * Error the OK or error code 16 | static func copy(src, dst, buff_size=64*1024): 17 | var fsrc = File.new() 18 | var err = fsrc.open(src, File.READ) 19 | if OK == err: 20 | var dir = Directory.new() 21 | if not dir.dir_exists(dst.get_base_dir()): 22 | err = dir.make_dir_recursive(dst.get_base_dir()) 23 | if OK == err: 24 | var fdst = File.new() 25 | err = fdst.open(dst, File.WRITE) 26 | if OK == err: 27 | var page = 0 28 | while fsrc.get_position() < fsrc.get_len(): 29 | var sizeleft = fsrc.get_len() - fsrc.get_position() 30 | var lenght = buff_size if sizeleft > buff_size else sizeleft 31 | var buff = fsrc.get_buffer(lenght) 32 | fdst.store_buffer(buff) 33 | page += 1 34 | fdst.close() 35 | fsrc.close() 36 | return err 37 | -------------------------------------------------------------------------------- /input/GestureDetector.gd: -------------------------------------------------------------------------------- 1 | # This Node is used to detect gestures 2 | 3 | tool 4 | extends Node 5 | 6 | enum SlideDetectMethod { DISTANCE, SPEED } 7 | export(SlideDetectMethod) var silde_detect_method = SlideDetectMethod.SPEED 8 | export var slide_distance = 100 9 | export var slide_speed = 600 10 | 11 | enum SlideGesture { 12 | SLIDE_NONE, 13 | SLIDE_UP, 14 | SLIDE_DOWN, 15 | SLIDE_LEFT, 16 | SLIDE_RIGHT 17 | } 18 | 19 | signal slide(dir) 20 | 21 | var _touch_down = false 22 | var _down_pos = Vector2() 23 | var _accept_slide = false 24 | 25 | func _input(event: InputEvent): 26 | if event is InputEventScreenTouch: 27 | _touch_down = event.pressed 28 | if _touch_down: 29 | _down_pos = event.position 30 | _accept_slide = true 31 | else: 32 | _accept_slide = false 33 | if _touch_down and event is InputEventMouseMotion: 34 | if _accept_slide: 35 | var dir = SlideGesture.SLIDE_NONE 36 | # detect slide by speed 37 | if silde_detect_method == SlideDetectMethod.SPEED: 38 | var speed = event.speed 39 | var speed_abs = speed.abs() 40 | var max_side_speed = max(speed_abs.x, speed_abs.y); 41 | if max_side_speed >= slide_speed: 42 | var side_speed = speed.x 43 | if speed_abs.y == max_side_speed: 44 | side_speed = speed.y 45 | dir = SlideGesture.SLIDE_DOWN if side_speed > 0 else SlideGesture.SLIDE_UP 46 | else: 47 | dir = SlideGesture.SLIDE_RIGHT if side_speed > 0 else SlideGesture.SLIDE_LEFT 48 | # detect slide by move distance 49 | elif silde_detect_method == SlideDetectMethod.DISTANCE: 50 | var delta = event.position - _down_pos 51 | var delta_abs = delta.abs() 52 | var max_side = max(delta_abs.x, delta_abs.y) 53 | if max_side > slide_distance: 54 | if max_side == delta_abs.x: 55 | dir = SlideGesture.SLIDE_RIGHT if delta.x > 0 else SlideGesture.SLIDE_LEFT 56 | else: 57 | dir = SlideGesture.SLIDE_DOWN if delta.y > 0 else SlideGesture.SLIDE_UP 58 | if dir != SlideGesture.SLIDE_NONE: 59 | self._accept_slide = false; 60 | self.emit_signal("slide", dir); 61 | -------------------------------------------------------------------------------- /input/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const GestureDetector = preload("GestureDetector.gd") 5 | -------------------------------------------------------------------------------- /plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="gdutils" 4 | description="Adds a new Heart node in 2D" 5 | author="Geequlim" 6 | version="0.0.1" 7 | script="plugin.gd" -------------------------------------------------------------------------------- /plugin.gd: -------------------------------------------------------------------------------- 1 | ################################################################################## 2 | # Tool generated DO NOT modify # 3 | ################################################################################## 4 | # This file is part of # 5 | # GodotExplorer # 6 | # https://github.com/GodotExplorer # 7 | ################################################################################## 8 | # Copyright (c) 2017-2018 Godot Explorer # 9 | # # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy # 11 | # of this software and associated documentation files (the "Software"), to deal # 12 | # in the Software without restriction, including without limitation the rights # 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # 14 | # copies of the Software, and to permit persons to whom the Software is # 15 | # furnished to do so, subject to the following conditions: # 16 | # # 17 | # The above copyright notice and this permission notice shall be included in all # 18 | # copies or substantial portions of the Software. # 19 | # # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # 26 | # SOFTWARE. # 27 | ################################################################################## 28 | 29 | tool 30 | extends EditorPlugin 31 | const gdutils = preload("__init__.gd") 32 | var tools = preload("editor/tools.gd").new() 33 | 34 | func _enter_tree(): 35 | add_custom_type("AsyncHttpTextureRect", "TextureRect", gdutils.scene.gui.AsyncHttpTextutreRect, null) 36 | add_custom_type("ItemListEnhanced", "ItemList", gdutils.scene.gui.ItemListEnhanced, null) 37 | add_custom_type("InfinityList", "ScrollContainer", gdutils.scene.gui.InfinityList, null) 38 | add_custom_type("GridBackground", "TextureRect", gdutils.scene.gui.GridBackground, null) 39 | 40 | func _ready(): 41 | tools.initialize(self) 42 | 43 | func _exit_tree(): 44 | remove_custom_type("AsyncHttpTextureRect") 45 | remove_custom_type("AutoLayoutControl") 46 | remove_custom_type("ItemListEnhanced") 47 | remove_custom_type("InfinityList") 48 | remove_custom_type("GridBackground") 49 | -------------------------------------------------------------------------------- /resource/GridBackgroundTexture.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | extends ImageTexture 4 | 5 | export var color1 = Color("#666666") setget _set_color1 6 | export var color2 = Color("#999999") setget _set_color2 7 | 8 | export(int) var cell_size = 10 setget _set_cell_size 9 | 10 | func _init(): 11 | _update_image() 12 | 13 | func _set_cell_size(p_size): 14 | cell_size = p_size 15 | _update_image() 16 | 17 | func _set_color1(p_color): 18 | color1 = p_color 19 | _update_image() 20 | 21 | func _set_color2(p_color): 22 | color2 = p_color 23 | _update_image() 24 | 25 | func _update_image(): 26 | var image = Image.new() 27 | image.create(cell_size * 2, cell_size * 2, false, Image.FORMAT_RGBA8) 28 | image.fill(color2) 29 | image.lock() 30 | for x in range(cell_size): 31 | for y in range(cell_size): 32 | image.set_pixel(x, y, color1) 33 | for x in range(cell_size, cell_size * 2): 34 | for y in range(cell_size, cell_size * 2): 35 | image.set_pixel(x, y, color1) 36 | image.unlock() 37 | create_from_image(image) 38 | -------------------------------------------------------------------------------- /resource/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const GridBackgroundTexture = preload("GridBackgroundTexture.gd") 5 | const image = preload("image.gd") 6 | 7 | # Load text file content as string 8 | # - - - 9 | # **parameter** 10 | # * [path: String] file path to load 11 | # - - - 12 | # **Return** 13 | # * String file content 14 | static func load_text_content(path): 15 | var file = File.new() 16 | if OK == file.open(path, File.READ): 17 | return file.get_as_text() 18 | return "" 19 | 20 | # Save text into file 21 | # - - - 22 | # **parameter** 23 | # * [text: String] The text content to save 24 | # * [path: String] The file path to save with 25 | # - - - 26 | # **Return** 27 | # * in : OK | error code 28 | static func save_text_content(text, path): 29 | var file = File.new() 30 | var err = file.open(path, File.WRITE) 31 | if OK == err: 32 | file.store_string(text) 33 | return err 34 | 35 | -------------------------------------------------------------------------------- /resource/image.gd: -------------------------------------------------------------------------------- 1 | # some useful functions for image files 2 | 3 | tool 4 | 5 | # Load image texture from external path 6 | # - - - 7 | # **Parameters** 8 | # * [path: String] The absolute image path 9 | # - - - 10 | # **Returns** 11 | # * ImageTexture | null 12 | static func load_image_texture(path): 13 | var img = Image.new() 14 | if OK == img.load(path): 15 | var tex = ImageTexture.new() 16 | tex.create_from_image(img) 17 | return tex 18 | return null 19 | 20 | # Save image into png file 21 | # - - - 22 | # **Parameters** 23 | # * [texture: Image|ImageTexture] The image or texture to save 24 | # * [path: String] The file path to save with 25 | # - - - 26 | # **Returns** 27 | # * [int] OK | error code 28 | static func save_image_texture(texture, path): 29 | if texture == null or typeof(texture) != TYPE_OBJECT or typeof(path) != TYPE_STRING: 30 | return ERR_INVALID_PARAMETER 31 | var img = null 32 | if texture is Image: 33 | img = texture 34 | elif texture is ImageTexture: 35 | img = texture.get_data() 36 | if img != null: 37 | return img.save_png(path) 38 | return ERR_INVALID_DATA 39 | -------------------------------------------------------------------------------- /resource/resource.gd: -------------------------------------------------------------------------------- 1 | # Load text file content as string 2 | # - - - 3 | # **parameter** 4 | # * [path: String] file path to load 5 | # - - - 6 | # **Return** 7 | # * String file content 8 | static func load_text_content(path): 9 | var file = File.new() 10 | if OK == file.open(path, File.READ): 11 | return file.get_as_text() 12 | return "" 13 | 14 | # Save text into file 15 | # - - - 16 | # **parameter** 17 | # * [text: String] The text content to save 18 | # * [path: String] The file path to save with 19 | # - - - 20 | # **Return** 21 | # * in : OK | error code 22 | static func save_text_content(text, path): 23 | var file = File.new() 24 | var err = file.open(path, File.WRITE) 25 | if OK == err: 26 | file.store_string(text) 27 | return err 28 | -------------------------------------------------------------------------------- /scene/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const gui = preload("gui/__init__.gd") 5 | -------------------------------------------------------------------------------- /scene/gui/AsyncHttpTextutreRect.gd: -------------------------------------------------------------------------------- 1 | # The control to display remote picture via http request 2 | 3 | extends "AsyncTextureRect.gd" 4 | var http = preload("../../utils/http.gd").new() 5 | 6 | func _init().(): 7 | http.name = "http" 8 | http.connect("request_completed", self, '_on_http_request_done') 9 | http.connect("request_failed", self, '_on_http_request_failed') 10 | http.connect("request_progress", self, '_on_http_request_progress') 11 | add_child(http) 12 | 13 | func _async_load_url(): 14 | if OK == http.request(url): 15 | self.state = State.LOADING 16 | else: 17 | self.state = State.FAILED 18 | 19 | func _on_http_request_done(p_url, headers, body): 20 | if p_url == self.url: 21 | var img = http.resolve_respose_body(headers, body) 22 | if typeof(img) == TYPE_OBJECT and img is Image: 23 | var tex = ImageTexture.new() 24 | tex.create_from_image(img) 25 | self.texture = tex 26 | self.state = State.SUCCESS 27 | else: 28 | self.state = State.FAILED 29 | 30 | func _on_http_request_failed(p_url, result, response_code): 31 | if url == p_url: 32 | self.state = State.FAILED 33 | 34 | func _on_http_request_progress(p_url, value, max_size): 35 | if p_url == self.url and progress_node != null: 36 | if progress_node.has_method('set_max'): 37 | progress_node.set_max(max_size) 38 | if progress_node.has_method('set_value'): 39 | progress_node.set_value(value) 40 | -------------------------------------------------------------------------------- /scene/gui/AsyncTextureRect.gd: -------------------------------------------------------------------------------- 1 | # The control to load texture in background 2 | 3 | tool 4 | extends TextureRect 5 | 6 | enum State { 7 | IDLE, 8 | LOADING, 9 | FAILED, 10 | SUCCESS 11 | } 12 | 13 | 14 | var container = CenterContainer.new() 15 | var state = State.IDLE setget _set_state 16 | export var url = "" setget _set_url 17 | export(PackedScene) var progress_template = null 18 | var progress_node = null 19 | export(PackedScene) var failed_template = null 20 | var failed_node = null 21 | var _pending_load = false 22 | 23 | func _init(): 24 | container.name = "PresetControls" 25 | container.set_anchors_and_margins_preset(Control.PRESET_WIDE) 26 | container.mouse_filter = Control.MOUSE_FILTER_IGNORE 27 | add_child(container) 28 | 29 | func _set_url(value): 30 | if url == value and (state in [State.LOADING, State.SUCCESS]): 31 | return 32 | url = value 33 | self.texture = null 34 | if not value.empty(): 35 | _pending_load = true 36 | set_process(true) 37 | 38 | # *virtual* Do async load action here don't forget change state 39 | func _async_load_url(): 40 | pass 41 | 42 | func _set_state(p_state): 43 | state = p_state 44 | if progress_node: progress_node.hide() 45 | if failed_node: failed_node.hide() 46 | match state: 47 | State.LOADING: 48 | if progress_node: progress_node.show() 49 | State.FAILED: 50 | if failed_node: failed_node.show() 51 | 52 | func _ready(): 53 | if progress_template != null: 54 | var node = progress_template.instance() 55 | if node != null: 56 | progress_node = node 57 | progress_node.name = "progress" 58 | container.add_child(progress_node) 59 | if progress_node.has_method('set_min'): 60 | progress_node.set_min(0) 61 | if failed_template != null: 62 | var node = failed_template.instance() 63 | if node != null: 64 | failed_node = node 65 | failed_node.name = "failed" 66 | container.add_child(failed_node) 67 | # initialize properties 68 | self.state = State.IDLE 69 | self.url = url 70 | 71 | func _process(delta): 72 | if _pending_load: 73 | _pending_load = false 74 | set_process(false) 75 | # Don't load the image in the editor as it will save the data to the scene 76 | if Engine.editor_hint: 77 | printt("AsyncTextureRect: ignored loading url ", url) 78 | return 79 | # run async load action 80 | _async_load_url() 81 | -------------------------------------------------------------------------------- /scene/gui/GridBackground.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends TextureRect 3 | export var color1 = Color("#666666") setget _set_color1 4 | export var color2 = Color("#999999") setget _set_color2 5 | export(int) var cell_size = 10 setget _set_cell_size 6 | 7 | var grid_texture = preload("../../resource/GridBackgroundTexture.gd").new() 8 | 9 | func _init(): 10 | self.texture = grid_texture 11 | self.expand = true 12 | self.stretch_mode = TextureRect.STRETCH_TILE 13 | 14 | func _set_color1(p_color): 15 | color1 = p_color 16 | grid_texture.color1 = p_color 17 | 18 | func _set_color2(p_color): 19 | color2 = p_color 20 | grid_texture.color2 = p_color 21 | 22 | func _set_cell_size(p_size): 23 | cell_size = p_size 24 | var cell_size_dpi_related = int(p_size * OS.get_screen_dpi() / 72.0) 25 | grid_texture.cell_size = cell_size_dpi_related 26 | 27 | 28 | -------------------------------------------------------------------------------- /scene/gui/InfinityList.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ScrollContainer 3 | 4 | signal end_reached() 5 | signal begin_readched() 6 | 7 | export(PackedScene) var item_template 8 | export(PackedScene) var header_template 9 | export(PackedScene) var footer_template 10 | export(int, 0, 1000) var space = 0 11 | export(int, 1, 60) var CACHE_SIZE = 1 12 | export var expand_size_to_parent = false 13 | 14 | export(int, "Horizontal", "Vertical") var direction = VERTICAL setget _set_direction 15 | var data_source = [] setget _set_data_source 16 | 17 | var _container = Control.new() 18 | var _item_size = Vector2() 19 | var _item_node_cache = [] 20 | var _queue_updating = true 21 | var _queue_updating_layout = true 22 | var _last_frame_scroll = 0 23 | var _footer = null 24 | var _header = null 25 | var _header_size = Vector2() 26 | var _footer_size = Vector2() 27 | var _last_top_index = -1 28 | 29 | # Queue update the visiable items of the list 30 | # If `item_count_changed` is `true` the content size will be updated 31 | func queue_update(item_count_changed = false): 32 | _queue_updating = true 33 | if item_count_changed: 34 | self.data_source = data_source 35 | 36 | # Force update the list 37 | func queue_update_layout(): 38 | _queue_updating_layout = true 39 | 40 | # Show the item 41 | func move_to_item(data): 42 | var index = data_source.find(data) 43 | var pos = _get_node_pos_by_index(index) 44 | if pos.x > 0: 45 | self.scroll_horizontal = pos.x 46 | if pos.y > 0: 47 | self.scroll_vertical = pos.y 48 | 49 | # Move to begin position of the list 50 | func move_to_begin(): 51 | self.scroll_vertical = 0 52 | self.scroll_horizontal = 0 53 | 54 | # Get footer node instance 55 | func get_footer(): 56 | return _footer 57 | 58 | # Get header node instance 59 | func get_header(): 60 | return _header 61 | 62 | var _size_calculate = null# Item to calculate the item size 63 | 64 | func _init(): 65 | connect("resized", self, "queue_update_layout") 66 | _container.mouse_filter = Control.MOUSE_FILTER_IGNORE 67 | add_child(_container) 68 | self._queue_updating = true 69 | 70 | func _enter_tree(): 71 | # header 72 | if _header == null: 73 | if typeof(header_template) == TYPE_OBJECT and header_template is PackedScene: 74 | _header = header_template.instance() 75 | _container.add_child(_header) 76 | # footer 77 | if _footer == null: 78 | if typeof(footer_template) == TYPE_OBJECT and footer_template is PackedScene: 79 | _footer = footer_template.instance() 80 | _container.add_child(_footer) 81 | # item for item size calculate 82 | if _size_calculate == null and typeof(item_template) == TYPE_OBJECT and item_template is PackedScene: 83 | _size_calculate = item_template.instance() 84 | queue_update_layout() 85 | # queue_update() 86 | 87 | func _exit_tree(): 88 | if _size_calculate != null: 89 | _size_calculate.free() 90 | _size_calculate = null 91 | 92 | func _set_data_source(ds): 93 | data_source = ds 94 | _queue_updating_layout = true 95 | 96 | func _set_direction(dir): 97 | direction = dir 98 | queue_update_layout() 99 | 100 | var override_size = Vector2() 101 | func _process(delta): 102 | if override_size == Vector2(): 103 | override_size = self.rect_size 104 | if expand_size_to_parent and get_parent() is Control: 105 | override_size = get_parent().rect_size 106 | if self.rect_size != override_size: 107 | _queue_updating_layout = true 108 | # update layout 109 | if _queue_updating_layout: 110 | _update_layout() 111 | _queue_updating = true 112 | _queue_updating_layout = false 113 | # update items 114 | var scroll = self.scroll_horizontal if direction == HORIZONTAL else self.scroll_vertical 115 | if not _queue_updating: 116 | _queue_updating = scroll != _last_frame_scroll 117 | _last_frame_scroll = scroll 118 | if _queue_updating: 119 | _update_items(scroll - _last_frame_scroll) 120 | _queue_updating = false 121 | # footer & header size 122 | if _footer != null and _footer.rect_size != _footer_size: 123 | _footer.rect_size = _footer_size 124 | if _header != null and _header.rect_size != _header_size: 125 | _header.rect_size = _header_size 126 | # item size 127 | for item in _item_node_cache: 128 | if item.is_inside_tree() and item.rect_size != _item_size: 129 | item.rect_size = _item_size 130 | 131 | func _update_items(scroll): 132 | var render_count = _item_node_cache.size() 133 | var top_index = _get_top_line_index() 134 | # replace front and back elements if top item changed 135 | var moved_step = top_index - _last_top_index 136 | _last_top_index = top_index 137 | if moved_step > 0: 138 | for i in range(moved_step): 139 | var front = _item_node_cache[0] 140 | _item_node_cache.pop_front() 141 | _item_node_cache.push_back(front) 142 | elif moved_step < 0: 143 | for i in range(abs(moved_step)): 144 | var back = _item_node_cache[-1] 145 | _item_node_cache.pop_back() 146 | _item_node_cache.push_front(back) 147 | # update item data if necessury 148 | for i in range(render_count): 149 | var index = top_index + i 150 | var node = _item_node_cache[i] 151 | node.rect_position = _get_node_pos_by_index(index) 152 | if index < data_source.size() and index >= 0: 153 | var data = data_source[index] 154 | if node.data != data: 155 | node.data = data 156 | if node.rect_size != _item_size: 157 | node.rect_size = _item_size 158 | node.show() 159 | else: 160 | node.hide() 161 | if _footer: 162 | if direction == VERTICAL: 163 | _footer.rect_position = Vector2(0, _container.rect_min_size.y - _footer_size.y) 164 | elif direction == HORIZONTAL: 165 | _footer.rect_position = Vector2(_container.rect_min_size.x - _footer_size.x, 0) 166 | # emit end/begin reached signal 167 | if moved_step != 0: 168 | if top_index + get_page_size() + moved_step > data_source.size(): 169 | emit_signal("end_reached") 170 | elif top_index + moved_step < 0: 171 | emit_signal("begin_readched") 172 | 173 | 174 | func _update_layout(): 175 | _update_size() 176 | _alloc_cache_nodes() 177 | 178 | func _update_size(): 179 | # update item size and footer/header size 180 | if true: 181 | var size = Vector2() 182 | var target_size = override_size 183 | if _size_calculate != null: 184 | var node = _size_calculate 185 | size = node.rect_min_size 186 | if node.size_flags_horizontal & Control.SIZE_EXPAND: 187 | size.x = max(target_size.x, size.x) 188 | if node.size_flags_vertical & Control.SIZE_EXPAND: 189 | size.y = max(target_size.y, size.y) 190 | # printt(node.size_flags_horizontal, node.size_flags_vertical, node.rect_min_size, target_size, size) 191 | if self._header: 192 | _header_size = _header.rect_min_size 193 | if _header.size_flags_horizontal & Control.SIZE_EXPAND: 194 | _header_size.x = max(target_size.x, _header_size.x) 195 | if _header.size_flags_vertical & Control.SIZE_EXPAND: 196 | _header_size.y = max(target_size.y, _header_size.y) 197 | if _header.rect_size != _header_size: 198 | _header.rect_size = _header_size 199 | if self._footer: 200 | _footer_size = _footer.rect_min_size 201 | if _footer.size_flags_horizontal & Control.SIZE_EXPAND: 202 | _footer_size.x = max(target_size.x, _footer_size.x) 203 | if _footer.size_flags_vertical & Control.SIZE_EXPAND: 204 | _footer_size.y = max(target_size.y, _footer_size.y) 205 | self._item_size = size 206 | # update container size 207 | if typeof(data_source) in [TYPE_ARRAY]: 208 | var min_size = Vector2() 209 | var space_size = (data_source.size() - 1) * space 210 | if space_size < 0: 211 | space_size = 0 212 | if direction == VERTICAL: 213 | min_size = Vector2(_item_size.x, _item_size.y * data_source.size() + space_size) 214 | if _header: min_size.y += _header_size.y 215 | if _footer: min_size.y += _footer_size.y 216 | min_size.x -= get_v_scrollbar().rect_size.x 217 | elif direction == HORIZONTAL: 218 | min_size = Vector2(_item_size.x * data_source.size() + space_size, _item_size.y) 219 | if _header: min_size.x += _header_size.x 220 | if _footer: min_size.x += _footer_size.x 221 | min_size.y -= get_h_scrollbar().rect_size.y 222 | _container.rect_min_size = min_size 223 | # update rect_size 224 | self.rect_size = override_size 225 | 226 | func _alloc_cache_nodes(): 227 | var cache_size = 0 228 | if direction == VERTICAL: 229 | cache_size = round(override_size.y / (_item_size.y + space)) + CACHE_SIZE 230 | elif direction == HORIZONTAL: 231 | cache_size = round(override_size.x / (_item_size.x + space)) + CACHE_SIZE 232 | var cur_cache_size = _item_node_cache.size() 233 | if cur_cache_size < cache_size: 234 | _item_node_cache.resize(cache_size) 235 | for i in range(cache_size): 236 | if _item_node_cache[i] == null: 237 | var node = item_template.instance() 238 | _item_node_cache[i] = node 239 | _container.add_child(node) 240 | node.rect_size = _item_size 241 | 242 | func _clear(): 243 | for node in _item_node_cache: 244 | node.queue_free() 245 | _item_node_cache.clear() 246 | 247 | func _get_top_line_index(): 248 | var index = -1 249 | if data_source.size(): 250 | if direction == VERTICAL: 251 | index = floor((self.scroll_vertical - _header_size.y) / (_item_size.y + space)) 252 | elif direction == HORIZONTAL: 253 | index = floor((self.scroll_horizontal - _header_size.x) / (_item_size.x + space)) 254 | if index >= data_source.size(): 255 | index = -1 256 | return index 257 | 258 | func _get_node_pos_by_index(index): 259 | var pos = Vector2() 260 | if index >= 0 and index < data_source.size(): 261 | if direction == VERTICAL: 262 | pos.x = 0 263 | pos.y = (_item_size.y + space) * index - space 264 | if _header: 265 | pos.y += _header_size.y 266 | elif direction == HORIZONTAL: 267 | pos.y = 0 268 | pos.x = (_item_size.x + space) * index - space 269 | if _header: 270 | pos.x += _header_size.x 271 | return pos 272 | 273 | func get_page_size(): 274 | if direction == VERTICAL: 275 | return ceil(override_size.y / (_item_size.y + space)) 276 | elif direction == HORIZONTAL: 277 | return ceil(override_size.x / (_item_size.x + space)) 278 | -------------------------------------------------------------------------------- /scene/gui/ItemListEnhanced.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ItemList 3 | 4 | signal double_clicked() 5 | signal mouse_right_clicked() 6 | signal selection_changed(selected_items) 7 | 8 | const ACCEPT_DS_TYPES = [ 9 | TYPE_ARRAY, 10 | TYPE_INT_ARRAY, 11 | TYPE_REAL_ARRAY, 12 | TYPE_COLOR_ARRAY, 13 | TYPE_STRING_ARRAY, 14 | TYPE_VECTOR2_ARRAY, 15 | TYPE_VECTOR3_ARRAY 16 | ] 17 | 18 | func _init(): 19 | connect("item_selected", self, "__on_select_changed") 20 | connect("multi_selected", self, "__on_select_changed") 21 | connect("nothing_selected", self, "__on_select_changed") 22 | 23 | # ListItemProvider 24 | # The provider of the item list it decides how the data is shown in the list 25 | var provider = ListItemProvider.new() setget _set_provider 26 | func _set_provider(p): 27 | if typeof(p) == TYPE_OBJECT:# and p is ListItemProvider:# FIXME: BUG of GDScript? 28 | provider = p 29 | update_list() 30 | 31 | var menu_handler = MenuActionHandler.new() setget _set_menu_handler 32 | var _popupmenu = null 33 | func _set_menu_handler(h): 34 | menu_handler = h 35 | if _popupmenu != null: 36 | _popupmenu.queue_free() 37 | remove_child(_popupmenu) 38 | _popupmenu = null 39 | if typeof(h) == TYPE_OBJECT:# and h is MenuActionHandler:# FIXME: BUG of GDScript? 40 | _popupmenu = h.create_popupmenu() 41 | if _popupmenu != null: 42 | _popupmenu.connect("id_pressed", self, "_on_menu_id_pressed") 43 | add_child(_popupmenu) 44 | 45 | # Arrary 46 | # The item data source of the list 47 | var data_source = [] setget _set_data_source 48 | func _set_data_source(arr): 49 | data_source = arr 50 | update_list() 51 | 52 | # Variant 53 | # The filter to decide is item in the `data_source` should be shown in the list 54 | var filter = null setget _set_filter 55 | func _set_filter(f): 56 | filter = f 57 | update_list() 58 | 59 | 60 | # Emited while wating action trigged with the list view 61 | signal on_action_pressed(action) 62 | 63 | # Event gui actions to watch with 64 | # Type: Array actions to watch 65 | var watching_actions = [] 66 | 67 | # Force update the item list 68 | # This action will clear and re-order the items 69 | func update_list(): 70 | clear() 71 | if typeof(data_source) in ACCEPT_DS_TYPES: 72 | var data_for_show = data_source 73 | if provider.sort_required(data_source): 74 | data_for_show = [] 75 | for data in data_source: 76 | data_for_show.append(data) 77 | data_for_show.sort_custom(provider, 'sort') 78 | 79 | var index = 0 80 | for data in data_for_show: 81 | if not provider.item_fit_filter(data, filter): 82 | continue 83 | var text = provider.get_item_text(data) 84 | if typeof(text) == TYPE_STRING: 85 | add_item(text) 86 | set_item_metadata(index, data) 87 | set_item_icon(index, provider.get_item_icon(data)) 88 | var color = provider.get_item_background_color(data) 89 | if typeof(color) == TYPE_COLOR: 90 | set_item_custom_bg_color(index, color) 91 | index += 1 92 | 93 | # Update target item of in the list 94 | func update_item(p_item): 95 | for id in range(get_item_count()): 96 | if p_item == get_item_metadata(id): 97 | set_item_text(id, provider.get_item_text(p_item)) 98 | set_item_custom_bg_color(id, provider.get_item_background_color(p_item)) 99 | set_item_icon(id, provider.get_item_icon(p_item)) 100 | break 101 | 102 | # Select items, the items selected before will be unselected 103 | # items: Array items to select 104 | func selecte_items(p_items): 105 | if not typeof(p_items) in ACCEPT_DS_TYPES: 106 | p_items = [p_items] 107 | for id in get_selected_items(): 108 | unselect(id) 109 | var single = p_items.size() == 1 110 | var selected = false 111 | for item in p_items: 112 | for i in range(get_item_count()): 113 | if get_item_metadata(i) == item: 114 | select(i, single) 115 | selected = true 116 | if single: 117 | break 118 | if single and selected: 119 | break 120 | 121 | # Get selected item data in an array 122 | func get_selected_item_list(): 123 | var _items = [] 124 | for idx in get_selected_items(): 125 | _items.append(get_item_metadata(idx)) 126 | return _items 127 | 128 | func _gui_input(event): 129 | # Mouse button actions 130 | if event is InputEventMouseButton: 131 | if event.button_index == BUTTON_RIGHT and not event.pressed: 132 | if _popupmenu != null: 133 | _popupmenu.set_global_position(get_global_mouse_position()) 134 | var selectedItems = get_selected_item_list() 135 | for i in range(_popupmenu.get_item_count()): 136 | var id = _popupmenu.get_item_id(i) 137 | _popupmenu.set_item_disabled(i, not menu_handler.item_enabled(id, selectedItems)) 138 | _popupmenu.popup() 139 | emit_signal("mouse_right_clicked") 140 | elif event.doubleclick: 141 | emit_signal("double_clicked") 142 | # Watching actions 143 | if event.is_pressed(): 144 | for action in watching_actions: 145 | if typeof(action) == TYPE_STRING: 146 | if event.is_action(action): 147 | emit_signal("on_action_pressed", action) 148 | break 149 | elif typeof(action) == TYPE_OBJECT and action is InputEvent: 150 | if event.action_match(action): 151 | emit_signal("on_action_pressed", action) 152 | break 153 | # Popupmenu shortcuts 154 | if event.is_pressed() and _popupmenu != null: 155 | var selectedItems = get_selected_item_list() 156 | for i in range(_popupmenu.get_item_count()): 157 | var id = _popupmenu.get_item_id(i) 158 | var shortcut = _popupmenu.get_item_shortcut(i) 159 | if shortcut != null and shortcut is ShortCut and event.action_match(shortcut.shortcut): 160 | if not _popupmenu.is_item_disabled(i): 161 | menu_handler.id_pressed(id, selectedItems) 162 | 163 | func _on_menu_id_pressed(id): 164 | menu_handler.id_pressed(id, get_selected_item_list()) 165 | 166 | func __on_select_changed(useless=null, useless2=null): 167 | emit_signal("selection_changed", get_selected_item_list()) 168 | 169 | # The provider class for ItemList 170 | # The provider decide how the data source is shown in the item list 171 | class ListItemProvider: 172 | # If your list need sort items return `true` here then the list wil try to 173 | # sort items with `sort` while update 174 | func sort_required(p_data_source): 175 | return false 176 | 177 | # The sort function to be used for item sorting 178 | # 179 | # ------------- 180 | # @parameters 181 | # * p_item1 182 | # * p_item2 183 | # The item data to sort with 184 | # 185 | # ------------- 186 | # Return true if `p_item1` needs to be shown before `p_item2` 187 | func sort(p_item1, p_item2): 188 | return true 189 | 190 | # Check if the item should be shown with the given filter from the list 191 | # 192 | # ------------- 193 | # @parameters 194 | # * p_item 195 | # * p_filter 196 | # 197 | # ------------- 198 | # Return true if `p_item1` needs to be shown before `p_item2` 199 | func item_fit_filter(p_item, p_filter): 200 | return true 201 | 202 | func get_item_text(p_item): 203 | return str(p_item) 204 | 205 | func get_item_icon(p_item): 206 | return null 207 | 208 | func get_item_background_color(p_item): 209 | return Color(0, 0, 0, 0) 210 | 211 | # Right mouse button popup menu handler 212 | class MenuActionHandler: 213 | # Create the popup menu control for this list view 214 | func create_popupmenu(): 215 | return null 216 | # Check the item with id is enabled for selected items 217 | func item_enabled(id, selectedItems): 218 | return true 219 | # This method is call when the menu item is pressed or its shortcut is pressed 220 | func id_pressed(id, selectedItems): 221 | pass 222 | -------------------------------------------------------------------------------- /scene/gui/Menu.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Reference 3 | 4 | # Menu type 5 | enum { 6 | NORMAL, 7 | CHECKABLE, 8 | SEPARATOR 9 | } 10 | 11 | var type = NORMAL # Type of the menu item 12 | var icon = null # The icon texture 13 | var title = "" # The title text of the menu item 14 | var disabled = false # The item is disabled or enabled 15 | var checked = false # This item is checked if the type is CHECKABLE 16 | var children = [] # Array Submenus 17 | var parent_ref = null # Weakref The parent menu item 18 | signal item_pressed(item) # emitted on menu item is pressed 19 | 20 | func _init(p_title = ""): 21 | title = p_title 22 | 23 | # Get child menu item by title 24 | # @param [p_title:String] The title of the child menu 25 | # @return [Menu|Nil] Return the child menu item or null if not found 26 | func get_child(p_title): 27 | for m in children: 28 | if m.title == p_title: 29 | return m 30 | return null 31 | 32 | # Get children menu items 33 | # - - - - - - - - - - 34 | # *Returns* Array 35 | func get_children(): 36 | return self.children 37 | 38 | # Get parent menu 39 | # - - - - - - - - - - 40 | # *Returns* Menu 41 | # The parent menu instance 42 | func get_parent(): 43 | if parent_ref != null: 44 | return parent_ref.get_ref() 45 | return null 46 | 47 | # Reset the parent menu item 48 | # - - - - - - - - - - 49 | # *Parameters* 50 | # * [new_parent: Menu] The new parent menu item 51 | # - - - - - - - - - - 52 | # *Returns* Menu 53 | # * Return self menu after reparent 54 | func reparent(new_parent): 55 | var parent = get_parent() 56 | if parent == new_parent: 57 | return 58 | if parent != null: 59 | if self in parent.children: 60 | parent.children.erase(self) 61 | if new_parent != null: 62 | new_parent.children.append(self) 63 | parent_ref = weakref(new_parent) 64 | else: 65 | parent_ref = null 66 | return self 67 | 68 | # Remove child menu item 69 | # @param [menu:Menu] The child menu item to remove 70 | # - - - - - - - - - - 71 | # *Returns* Menu 72 | # * Return self menu item 73 | func remove_child(p_menu): 74 | p_menu.reparent(null) 75 | return self 76 | 77 | # Add child menu item 78 | # @param [menu:Menu] The child menu item 79 | # - - - - - - - - - - 80 | # *Returns* Menu 81 | # * Return self menu item 82 | func add_child(p_menu): 83 | p_menu.reparent(self) 84 | return self 85 | 86 | # Add a seperator menu item 87 | # - - - - - - - - - - 88 | # *Returns* Menu 89 | # * Return self menu item 90 | func add_separator(): 91 | var item = get_script().new() 92 | item.type = SEPARATOR 93 | add_child(item) 94 | return self 95 | 96 | # Create a sub-menu item 97 | # - - - - - - - - - - 98 | # *Parameters* 99 | # * [p_type: MenuType] The type of the menu item 100 | # * [p_title: String] The menu item title 101 | # - - - - - - - - - - 102 | # *Returns* Menu 103 | # * Return the created menu item 104 | func create_item(p_title=""): 105 | var item = get_script().new(p_title) 106 | add_child(item) 107 | return item 108 | 109 | # Get the full path of the menu item 110 | # @return [String] The menu path is seperated with `/` 111 | func get_path(): 112 | var path = title 113 | var parent = get_parent() 114 | while parent != null: 115 | path = str(parent.title, "/", path) 116 | parent = parent.get_parent() 117 | return path 118 | 119 | # Clone the menu item include its submenu items 120 | # - - - - - - - - - - 121 | # *Returns* Variant 122 | # * Return document 123 | func clone(): 124 | var menu = get_script().new() 125 | menu.type = type 126 | menu.icon = icon 127 | menu.title = title 128 | menu.disabled = disabled 129 | menu.checked = checked 130 | menu.parent_ref = parent_ref 131 | for item in children: 132 | menu.add_submenu(item.clone()) 133 | return menu 134 | 135 | # Find child menu item by path 136 | # - - - - - - - - - - 137 | # @param [path:String] The path of the child item emnu like `Open/Text File` 138 | # - - - - - - - - - - 139 | # @return [Menu|Nil] Return the found menu item or null if not found 140 | func find_item(path): 141 | if typeof(path) == TYPE_STRING and not path.empty(): 142 | var titles = path.split("/") 143 | var menu = self 144 | for t in titles: 145 | menu = menu.get_child(t) 146 | if menu == null: 147 | return null 148 | if menu != self and menu.title == titles[-1]: 149 | return menu 150 | return null 151 | 152 | # Render the menu(include all of the submenu) into a MenuButton control 153 | func render_to_menu_button(p_button): 154 | if typeof(p_button) == TYPE_OBJECT and p_button is MenuButton: 155 | p_button.set_text(title) 156 | render_to_popup(p_button.get_popup()) 157 | 158 | # Render the menu(include all of the submenu) into a PopupMenu node 159 | # @param [popup:PopupMenu] The PopupMenu node instance 160 | var popups = [] 161 | func render_to_popup(popup): 162 | if not children.empty() and typeof(popup) == TYPE_OBJECT and popup is PopupMenu: 163 | popup.clear() 164 | for subpop in popup.get_children(): 165 | if subpop is PopupMenu: 166 | subpop.queue_free() 167 | if not popup.is_connected("id_pressed", self, "_on_item_pressed"): 168 | popup.connect("id_pressed", self, "_on_item_pressed", [popup]) 169 | popups.append(popup) 170 | var idx = 0 171 | for item in children: 172 | if item.type == SEPARATOR: 173 | popup.add_separator() 174 | elif item.children.empty(): 175 | if item.type == CHECKABLE: 176 | if item.icon != null: 177 | popup.add_icon_check_item(item.icon, item.title, idx) 178 | else: 179 | popup.add_check_item(item.title, idx) 180 | popup.set_item_checked(idx, item.checked) 181 | elif item.icon != null: 182 | popup.add_icon_item(item.icon, item.title, idx) 183 | else: 184 | popup.add_item(item.title, idx) 185 | else: 186 | var subpop = PopupMenu.new() 187 | subpop.name = str(idx, '#',item.title) 188 | popup.add_child(subpop) 189 | popup.add_submenu_item(item.title, subpop.name, idx) 190 | item.render_to_popup(subpop) 191 | popup.set_item_metadata(idx, item) 192 | popup.set_item_disabled(idx, item.disabled) 193 | idx += 1 194 | 195 | # Load items form dictionary 196 | # - - - - - - - - - - 197 | # *Parameters* 198 | # * [dict: Dictionary] Menu configuration data 199 | # ```gdscript 200 | # menu.parse_dictionary({ 201 | # "title": "Export", 202 | # "icon": "res://icon.png" 203 | # "items": [ 204 | # { "title": "PNG File", "disabled": true}, 205 | # { "type": "separator"} 206 | # { "title": "JPG File", "type": "checkable", "checked": false} 207 | # ] 208 | # }) 209 | # ``` 210 | func parse_dictionary(dict): 211 | self.title = str(dict.title) if dict.has('title') else self.title 212 | self.disabled = bool(dict.disabled) if dict.has('disabled') else false 213 | self.icon = load(str(dict.icon)) if dict.has('icon') else null 214 | self.checked = bool(dict.checked) if dict.has('checked') else false 215 | if dict.has('type') and typeof(dict.type) == TYPE_STRING: 216 | if dict.type.to_lower() == 'separator': 217 | self.type = SEPARATOR 218 | elif dict.type.to_lower() in ['checkable', 'check', 'checkbox']: 219 | self.type = CHECKABLE 220 | if dict.has('items') and typeof(dict.items) == TYPE_ARRAY: 221 | for item in dict.items: 222 | if typeof(item) == TYPE_DICTIONARY: 223 | var m = get_script().new() 224 | m.parse_dictionary(item) 225 | add_child(m) 226 | 227 | func _on_item_pressed(idx, popup): 228 | var menu = popup.get_item_metadata(idx) 229 | var parent = self 230 | while parent != null: 231 | parent.emit_signal("item_pressed", menu) 232 | parent = parent.get_parent() 233 | -------------------------------------------------------------------------------- /scene/gui/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const AsyncHttpTextutreRect = preload("AsyncHttpTextutreRect.gd") 5 | const AsyncTextureRect = preload("AsyncTextureRect.gd") 6 | const GridBackground = preload("GridBackground.gd") 7 | const InfinityList = preload("InfinityList.gd") 8 | const ItemListEnhanced = preload("ItemListEnhanced.gd") 9 | const Menu = preload("Menu.gd") 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | --------------------------------------------------------------------------------- 6 | This file is part of 7 | GodotExplorer 8 | https://github.com/GodotExplorer 9 | --------------------------------------------------------------------------------- 10 | Copyright (c) 2017-2019 Godot Explorer 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | --------------------------------------------------------------------------------- 30 | ''' 31 | 32 | import re 33 | import fnmatch 34 | import os 35 | import sys 36 | 37 | CWD = os.path.abspath(os.path.dirname(__file__)) 38 | IGNORE_LIST = [ 39 | os.path.join(CWD, ".vscode"), 40 | os.path.join(CWD, "autoloads"), 41 | os.path.join(CWD, "plugin.gd") 42 | ] 43 | LIB_NAME = None 44 | 45 | def glob_path(path, pattern): 46 | result = [] 47 | for root, _, files in os.walk(path): 48 | for filename in files: 49 | if fnmatch.fnmatch(filename, pattern): 50 | result.append(os.path.join(root, filename)) 51 | return result 52 | 53 | def identify(name): 54 | newname = name 55 | if len(newname) > 0: 56 | if newname[:1] in '0123456789': 57 | newname = re.sub('\d', '_', newname, 1) 58 | newname = re.sub('[^\w]', '_', newname) 59 | return newname 60 | 61 | def main(): 62 | for f in glob_path(os.getcwd(), "__init__.gd"): 63 | try: 64 | if os.path.dirname(f) not in IGNORE_LIST: 65 | os.remove(f) 66 | except e: 67 | print("Failed remove file {} \n{}".format(f, e)) 68 | if not '-c' in sys.argv and not '--clean' in sys.argv: 69 | extract_dir(os.getcwd()) 70 | 71 | 72 | def extract_dir(root): 73 | if root in IGNORE_LIST: 74 | return None 75 | pathes = sorted(os.listdir(root)) 76 | content = "" 77 | licenseText = open(os.path.join(CWD, 'LICENSE')).read() 78 | delayExtracs = [] 79 | for p in pathes: 80 | path = os.path.join(root,p) 81 | if path in IGNORE_LIST: 82 | continue 83 | path = path.replace("./", "").replace(".\\", "") 84 | if os.path.isfile(path) and path.endswith(".gd") and not path.endswith('__init__.gd'): 85 | if os.path.basename(root) + ".gd" == os.path.basename(path): 86 | delayExtracs.append((root, path)) 87 | else: 88 | content += gen_expression(root, path) 89 | elif os.path.isdir(path): 90 | subdirfile = extract_dir(path) 91 | if subdirfile is not None: 92 | content += gen_expression(root, subdirfile) 93 | for dp in delayExtracs: 94 | content += gen_expression(dp[0], dp[1]) 95 | if len(content) > 0: 96 | gdfile = os.path.join(root, '__init__.gd') 97 | try: 98 | toolprefix = '\ntool\n' 99 | if root == CWD: toolprefix += 'extends Node\n' 100 | if LIB_NAME: toolprefix += 'class_name {}\n'.format(LIB_NAME) 101 | open(gdfile,'w').write(toolprefix + "\n" + content) 102 | except e: 103 | raise e 104 | return gdfile 105 | return None 106 | 107 | def gen_expression(root, filepath): 108 | path = os.path.relpath(filepath, root) 109 | path = path.replace('\\', '/') 110 | name = identify(os.path.basename(path)[:-3]) 111 | if os.path.basename(path) == '__init__.gd': 112 | name = identify(os.path.basename(os.path.dirname(filepath))) 113 | if os.path.basename(root) + ".gd" == os.path.basename(filepath): 114 | return '\n{}\n'.format(open(filepath).read()) 115 | else: 116 | return 'const {} = preload("{}")\n'.format(name, path) 117 | 118 | if __name__ == '__main__': 119 | main() -------------------------------------------------------------------------------- /utils/AsyncTaskQueue.gd: -------------------------------------------------------------------------------- 1 | # The task queue to run tasks in a seperated thread 2 | 3 | tool 4 | var running = false 5 | var frame_interval = 1 6 | var thread = Thread.new() 7 | var task_queue = [] 8 | 9 | func post(object, method, params = [], callback_caller = null, callback = ''): 10 | var mutex = Mutex.new() 11 | mutex.lock() 12 | task_queue.push_back([object.get_instance_id(), method, params, callback_caller.get_instance_id() if callback_caller != null else 0, callback]) 13 | mutex.unlock() 14 | 15 | func cancel(object, method, callback_caller = null, callback = ''): 16 | var mutex = Mutex.new() 17 | mutex.lock() 18 | var instance_id = object.get_instance_id() 19 | var cancel_tasks = [] 20 | for task in task_queue: 21 | if task != null: 22 | if task[0] == instance_id and task[1] == method and callback_caller.get_instance_id() == task[3] and callback == task[4]: 23 | cancel_tasks.append(task) 24 | for t in cancel_tasks: 25 | task_queue.erase(t) 26 | mutex.unlock() 27 | 28 | func start(): 29 | running = true 30 | thread.start(self, "thread_main", get_instance_id()) 31 | 32 | func stop(): 33 | running = false 34 | thread.wait_to_finish() 35 | 36 | func thread_main(instance_id): 37 | var this = instance_from_id(instance_id) 38 | while this.running: 39 | if this.task_queue.size() > 0: 40 | var mutex = Mutex.new() 41 | mutex.lock() 42 | var task = this.task_queue[0] 43 | if task != null: 44 | this.task_queue.pop_front() 45 | mutex.unlock() 46 | mutex = null 47 | var instance = instance_from_id(task[0]) 48 | if instance != null: 49 | var ret = instance.callv(task[1], task[2]) 50 | var callback_caller = instance_from_id(task[3]) 51 | if callback_caller: 52 | callback_caller.call_deferred(task[4], ret) 53 | if mutex != null: 54 | mutex.unlock() 55 | OS.delay_usec(this.frame_interval) 56 | -------------------------------------------------------------------------------- /utils/InstanceManager.gd: -------------------------------------------------------------------------------- 1 | # Serialize and unserialize for GDScript instances 2 | 3 | tool 4 | const json = preload('json.gd') 5 | 6 | const TYPE_ATOMIC_PREFIX = "@Atomic:" 7 | const LEN_TYPE_ATOMIC_PREFIX = 8 8 | 9 | const TYPE_OBJECT_PREFIX = "@Object:" 10 | const LEN_TYPE_OBJECT_PREFIX = 8 11 | 12 | const TYPE_REF_PREFIX = "@Reference:" 13 | const LEN_TYPE_REF_PREFIX = 11 14 | 15 | const MEATA_NAME_INST_UID = "@InstanceID" 16 | 17 | # Atomic types that json doesn't support 18 | const SERIALIZED_ATOMIC_TYPES = [ 19 | TYPE_VECTOR2, 20 | TYPE_RECT2, 21 | TYPE_VECTOR3, 22 | TYPE_TRANSFORM2D, 23 | TYPE_PLANE, 24 | TYPE_QUAT, 25 | TYPE_AABB, 26 | TYPE_BASIS, 27 | TYPE_TRANSFORM, 28 | TYPE_COLOR, 29 | ] 30 | 31 | var pool = {} 32 | var data = {} 33 | 34 | # Put data for saving 35 | # Note: The key is converted into String so please make it as simple as possiable 36 | # - - - 37 | # **Parameters** 38 | # * p_key: Variant The key is string value expected 39 | # * value: Variant The data to save with 40 | func put(p_key, value): 41 | data[str(p_key)] = _serialize(value) 42 | 43 | # Get loaded data by a key 44 | # - - - 45 | # **Parameters** 46 | # * p_key: Variant The key of the data was saved 47 | # - - - 48 | # **Returns** 49 | # * Variant 50 | func get(p_key): 51 | var key = str(p_key) 52 | if data.has(key): 53 | return data[key] 54 | return .get(key) 55 | 56 | # Load the saved json file 57 | func load(path): 58 | return parse_serialized_dict(json.load_json(path)) 59 | 60 | # Save all data into json 61 | func save(path): 62 | return json.save_json(serialize_to_dict(), path) 63 | 64 | # Get a dictionary that contains all serialized instances 65 | func serialize_to_dict(): 66 | return {"Objects": pool, "data": data } 67 | 68 | # parse intances from serialized dictionary 69 | func parse_serialized_dict(dict): 70 | _reset() 71 | if dict.has("Objects"): 72 | # Step1: unserialize all the objects into the poll 73 | # Some of the objects after this step may not be completely unserialized 74 | # as they are circle referenced 75 | var poolDict = dict["Objects"] 76 | for id in poolDict: 77 | id = int(id) 78 | _parsing_instance_uid = id 79 | self.pool[id] = _unserialize(poolDict[str(id)], poolDict) 80 | # Step2: To resovle all objects that is not comletely unserialized in the poll 81 | # After step1 all object should be able to referenced to now 82 | for id in self.pool: 83 | _setup_references(self.pool[id]) 84 | # Step3: Unserailize the data user saved 85 | if dict.has("data"): 86 | var dataDict = dict["data"] 87 | for key in dataDict: 88 | self.data[key] = _unserialize(dataDict[key], poolDict) 89 | return OK 90 | return ERR_PARSE_ERROR 91 | 92 | # Deep clone a script instance, a dictionary or an array 93 | # - - - 94 | # **Parameters** 95 | # * inst: The data to copy from 96 | # - - - 97 | # **Returns** 98 | # The same structured data cloned from inst 99 | func deep_clone_instance(inst): 100 | if false:# For debug usage 101 | var im = get_script().new() 102 | im.put("o", inst) 103 | im.save("res://o.json") 104 | im = get_script().new() 105 | im.load("res://o.json") 106 | return im.get("o") 107 | var im = get_script().new() 108 | im.put("o", inst) 109 | var im2 = get_script().new() 110 | if OK == im2.parse_serialized_dict(parse_json(to_json(im.serialize_to_dict()))): 111 | return im2.get("o") 112 | return null 113 | 114 | ################################################## 115 | # private methods 116 | ################################################# 117 | var _parsing_instance_uid = 0 118 | var _max_instance_uid = 0 119 | var _instance_uid_map = {} 120 | var _uid_instance_map = {} 121 | 122 | func _reset(): 123 | pool.clear() 124 | data.clear() 125 | _instance_uid_map.clear() 126 | _uid_instance_map.clear() 127 | _parsing_instance_uid = 0 128 | _max_instance_uid = 0 129 | 130 | func _serialize(inst): 131 | var ret = inst 132 | if typeof(inst) in SERIALIZED_ATOMIC_TYPES: 133 | ret = str(TYPE_ATOMIC_PREFIX, var2str(inst)) 134 | elif typeof(inst) == TYPE_OBJECT: 135 | if inst is WeakRef: 136 | var obj = inst.get_ref() 137 | ret = _serialize(obj).replace(TYPE_OBJECT_PREFIX, TYPE_REF_PREFIX) if obj != null else null 138 | elif inst.get_script() != null: 139 | var uid = 0 140 | var is_replace = false 141 | var InstID = inst.get_instance_id() 142 | if not _instance_uid_map.has(InstID): 143 | uid = _max_instance_uid 144 | if inst.has_meta(MEATA_NAME_INST_UID): 145 | uid = inst.get_meta(MEATA_NAME_INST_UID) 146 | if self.pool.has(uid): 147 | is_replace = true 148 | var old_inst_id = _uid_instance_map[uid] 149 | _instance_uid_map[old_inst_id] = _max_instance_uid 150 | _uid_instance_map[_max_instance_uid] = old_inst_id 151 | self.pool[_max_instance_uid] = self.pool[uid] 152 | _instance_uid_map[InstID] = uid 153 | _uid_instance_map[uid] = InstID 154 | _max_instance_uid += 1 155 | else: 156 | uid = _instance_uid_map[InstID] 157 | if is_replace or (not self.pool.has(uid)): 158 | var dict = inst2dict(inst) 159 | self.pool[uid] = dict 160 | for key in dict: 161 | dict[key] = _serialize(dict[key]) 162 | ret = str(TYPE_OBJECT_PREFIX, uid) 163 | elif typeof(inst) == TYPE_ARRAY: 164 | ret = [] 165 | for ele in inst: 166 | ret.append(_serialize(ele)) 167 | elif typeof(inst) == TYPE_DICTIONARY: 168 | ret = {} 169 | for key in inst: 170 | ret[_serialize(key)] = _serialize(inst[key]) 171 | return ret 172 | 173 | func _unserialize(any, rawPool): 174 | var ret = any 175 | if typeof(any) == TYPE_REAL: 176 | if int(any) == any: 177 | ret = int(any) 178 | elif typeof(any) == TYPE_STRING: 179 | if any.begins_with(TYPE_OBJECT_PREFIX): 180 | var id = int(any.substr(LEN_TYPE_OBJECT_PREFIX, any.length())) 181 | if self.pool.has(id): 182 | ret = self.pool[id] 183 | else: 184 | self.pool[id] = any 185 | elif any.begins_with(TYPE_ATOMIC_PREFIX): 186 | var text = any.substr(LEN_TYPE_ATOMIC_PREFIX, any.length()) 187 | ret = str2var(text) 188 | elif typeof(any) == TYPE_DICTIONARY: 189 | for key in any: 190 | var prop = any[key] 191 | any[_unserialize(key, rawPool)] = _unserialize(prop, rawPool) 192 | if any.has("@path") and any.has("@subpath"): 193 | ret = dict2inst(any) 194 | if typeof(ret) == TYPE_OBJECT: 195 | ret.set_meta(MEATA_NAME_INST_UID, _parsing_instance_uid) 196 | elif typeof(any) == TYPE_ARRAY: 197 | ret = [] 198 | for ele in any: 199 | ret.append(_unserialize(ele, rawPool)) 200 | return ret 201 | 202 | func _setup_references(inst): 203 | var ret = inst 204 | if typeof(inst) == TYPE_OBJECT: 205 | for propDesc in inst.get_property_list(): 206 | if propDesc.usage & PROPERTY_USAGE_CATEGORY: continue 207 | var prop = inst.get(propDesc.name) 208 | if typeof(prop) in [TYPE_STRING, TYPE_ARRAY, TYPE_DICTIONARY]: 209 | inst.set(propDesc.name, _setup_references(prop)) 210 | elif typeof(inst) == TYPE_STRING: 211 | if inst.begins_with(TYPE_OBJECT_PREFIX): 212 | var id = int(inst.substr(LEN_TYPE_OBJECT_PREFIX, inst.length())) 213 | if self.pool.has(id): 214 | ret = self.pool[id] 215 | elif inst.begins_with(TYPE_REF_PREFIX): 216 | var id = int(inst.substr(LEN_TYPE_REF_PREFIX, inst.length())) 217 | if self.pool.has(id): 218 | ret = weakref(self.pool[id]) 219 | elif typeof(inst) == TYPE_ARRAY: 220 | ret = [] 221 | for ele in inst: 222 | ret.append(_setup_references(ele)) 223 | elif typeof(inst) == TYPE_DICTIONARY: 224 | ret = {} 225 | for key in inst: 226 | ret[_setup_references(key)] = _setup_references(inst[key]) 227 | return ret 228 | -------------------------------------------------------------------------------- /utils/ObjectPool.gd: -------------------------------------------------------------------------------- 1 | # A simple object pool to cache and reuse objects 2 | 3 | tool 4 | var objects = [] 5 | var _using_objects = {} 6 | 7 | # Pool size 8 | var size = 0 setget resize, get_size 9 | func get_size() -> int: 10 | return objects.size() 11 | 12 | # Using object count in the pool 13 | var using_count setget , get_using_count 14 | func get_using_count() -> int: 15 | var count = 0 16 | for i in range(objects.size()): 17 | if _using_objects[i]: 18 | count += 1 19 | return count 20 | 21 | func get_using_objects(): 22 | var ret = [] 23 | for i in _using_objects: 24 | if _using_objects[i]: 25 | ret.append(objects[i]) 26 | return ret 27 | 28 | # Set the pool size 29 | func resize(v: int): 30 | if v >= 0: 31 | var last_size = objects.size() 32 | if v > objects.size(): 33 | objects.resize(v) 34 | for i in range(last_size, v): 35 | objects[i] = create_object() 36 | _using_objects[i] = false 37 | elif v < last_size: 38 | for i in range(v, last_size): 39 | var obj = objects[i] 40 | destroy_object(obj) 41 | _using_objects[i] = false 42 | objects.resize(v) 43 | 44 | # Get an object from the pool 45 | func get_object() -> Object: 46 | for i in range(objects.size()): 47 | if not _using_objects[i]: 48 | _using_objects[i] = true 49 | return objects[i] 50 | var idx = objects.size() 51 | resize(idx + 1) 52 | _using_objects[idx] = true 53 | return objects[idx] 54 | 55 | # Release an object to the pool 56 | # The object must be returned from `get_object` method 57 | func release(p_object: Object): 58 | var idx = objects.find(p_object) 59 | if idx != -1: 60 | if _using_objects[idx]: 61 | release_object(p_object) 62 | _using_objects[idx] = false 63 | 64 | # Release all of the objects in the pool 65 | # Make all of the objects is usable 66 | # This method won't destroy objects 67 | func clear(): 68 | for i in range(objects.size()): 69 | if _using_objects[i]: 70 | release_object(objects[i]) 71 | _using_objects[i] = false 72 | 73 | # Destroy all of the objects in the pool 74 | func destroy(): 75 | clear() 76 | resize(0) 77 | _using_objects = {} 78 | 79 | # The method to override to create object for the pool 80 | func create_object() -> Object: 81 | return null 82 | 83 | # Call on an object is release(return back) to the pool 84 | func release_object(p_object: Object): 85 | pass 86 | 87 | # The method to override to destroy object for the pool 88 | func destroy_object(p_object: Object): 89 | if not p_object is Reference: 90 | if p_object is Node and p_object.is_inside_tree(): 91 | p_object.queue_free() 92 | else: 93 | p_object.free() 94 | -------------------------------------------------------------------------------- /utils/RandomPool.gd: -------------------------------------------------------------------------------- 1 | # A random pool to select random items in the pool with weight support 2 | 3 | tool 4 | class RandomItem: 5 | var weight: float = 1 6 | var start: float = 0 7 | var data = null 8 | 9 | var _items = [] 10 | var _next_item_weight_start: float = 0 11 | 12 | # Add item with weight to the random pool 13 | # - - - - - - - - - - 14 | # *Parameters* 15 | # * [data:Variant] The item data of the random item 16 | # * [weight:float] The weight of this item in the random pool 17 | func add_item(data, weight: float = 1): 18 | var w = abs(weight) 19 | var item = RandomItem.new() 20 | item.data = data 21 | item.weight = w 22 | item.start = self._next_item_weight_start 23 | self._next_item_weight_start += w 24 | _items.append(item) 25 | 26 | # Update the weight chain of the pool 27 | # - - - - - - - - - - 28 | # *Returns* float 29 | # * Return the sum of weights 30 | func update(): 31 | self._next_item_weight_start = 0 32 | for item in _items: 33 | item.start = self._next_item_weight_start 34 | self._next_item_weight_start += item.weight 35 | return self._next_item_weight_start 36 | 37 | 38 | # Random select item from the pool according to thier weight 39 | # - - - - - - - - - - 40 | # *Parameters* 41 | # * [count:int] number of items to get from the pool 42 | # * [norepeat:false] Allow repeat items in the returned items 43 | # - - - - - - - - - - 44 | # *Returns* Array 45 | # * Return the selected selected items 46 | func random(count: int = 1, norepeat = false) -> Array: 47 | var ret = [] 48 | if norepeat and count >= self._items.size(): 49 | for item in self._items: 50 | ret.append(item.data) 51 | ret.shuffle() 52 | return ret 53 | var selected_item: RandomItem = null 54 | var rand_num = rand_range(0, self._next_item_weight_start) 55 | for item in self._items: 56 | if rand_num >= item.start and rand_num < item.start + item.weight: 57 | selected_item = item 58 | break 59 | if count > 1: 60 | var pool = get_script().new() 61 | if norepeat: 62 | pool._items = self._items.duplicate() 63 | if selected_item: 64 | pool._items.erase(selected_item) 65 | else: 66 | pool._items = self._items 67 | pool.update() 68 | ret = pool.random(count - 1, norepeat) 69 | if selected_item: 70 | ret.append(selected_item.data) 71 | return ret 72 | -------------------------------------------------------------------------------- /utils/TexturePacker.gd: -------------------------------------------------------------------------------- 1 | # Simple tool to pack images into an atlas 2 | 3 | tool 4 | const json = preload("json.gd") 5 | 6 | var max_size = Vector2(2048, 2048) 7 | var source_images = {} 8 | var atlas_texture: Image = null 9 | var frames = {} 10 | var margin = Vector2(2, 2) 11 | 12 | func clear(): 13 | source_images = [] 14 | 15 | func add_image(id, image: Image): 16 | if image != null and image is Image: 17 | source_images[id] = image 18 | 19 | func pack(): 20 | self.frames = {} 21 | var temp_img = Image.new() 22 | temp_img.create(max_size.x, max_size.y, false, Image.FORMAT_RGBA8) 23 | var bottom = 0 24 | var right = 0 25 | var cur_frame_pos = Vector2() 26 | for id in source_images: 27 | var img = source_images[id] 28 | var image_size = img.get_size() 29 | if cur_frame_pos.y + image_size.y > max_size.y: break 30 | temp_img.blit_rect(img, Rect2(Vector2(), image_size), cur_frame_pos) 31 | self.frames[id] = Rect2(cur_frame_pos, image_size) 32 | bottom = max(cur_frame_pos.y + image_size.y + margin.y, bottom) 33 | right = max(right, cur_frame_pos.x + image_size.x) 34 | cur_frame_pos.x += image_size.x + margin.x 35 | if cur_frame_pos.x + image_size.x > max_size.x: 36 | cur_frame_pos.x = 0 37 | cur_frame_pos.y = bottom 38 | self.atlas_texture = Image.new() 39 | atlas_texture.create(right, bottom, false, Image.FORMAT_RGBA8) 40 | atlas_texture.blit_rect(temp_img, Rect2(Vector2(), Vector2(right, bottom)), Vector2()) 41 | return OK 42 | 43 | func export_atlas(path): 44 | var err = atlas_texture.save_png(path) 45 | if err == OK: 46 | var atlas = { 47 | "texture": path.get_file(), 48 | "frames": {}, 49 | "width": atlas_texture.get_width(), 50 | "height": atlas_texture.get_height(), 51 | } 52 | for frame in frames: 53 | var rect: Rect2 = frames[frame] 54 | atlas.frames[frame] = {"x": rect.position.x, "y": rect.position.y, "w": rect.size.x, "h": rect.size.y} 55 | var atlas_config_file = path.replace(".png", ".json") 56 | err = json.save_json(atlas, atlas_config_file) 57 | return err 58 | -------------------------------------------------------------------------------- /utils/__init__.gd: -------------------------------------------------------------------------------- 1 | 2 | tool 3 | 4 | const AsyncTaskQueue = preload("AsyncTaskQueue.gd") 5 | const InstanceManager = preload("InstanceManager.gd") 6 | const ObjectPool = preload("ObjectPool.gd") 7 | const RandomPool = preload("RandomPool.gd") 8 | const TexturePacker = preload("TexturePacker.gd") 9 | const csv = preload("csv.gd") 10 | const http = preload("http.gd") 11 | const json = preload("json.gd") 12 | const uuid = preload("uuid.gd") 13 | 14 | 15 | # Check does an object has all of script methods of target class 16 | # - - - - - - - - - - 17 | # *Parameters* 18 | # * [obj:Object] The object to check 19 | # * [interface:GDScript] The interface to check with 20 | # - - - - - - - - - - 21 | # *Returns* bool 22 | static func implements(obj: Object, interface:GDScript) -> bool: 23 | if obj == null or interface == null: 24 | return false 25 | if typeof(obj) != TYPE_OBJECT: 26 | return false 27 | if obj is interface: 28 | return true 29 | var interface_obj = interface.new() 30 | var required_methods = [] 31 | for m in interface_obj.get_method_list(): 32 | if m.flags & METHOD_FLAG_FROM_SCRIPT: 33 | required_methods.append(m.name) 34 | if not interface_obj is Reference: 35 | interface_obj.free() 36 | for mid in required_methods: 37 | if not obj.has_method(mid): 38 | return false 39 | return true 40 | 41 | # Format the time duration to a text to display 42 | # - - - - - - - - - - 43 | # *Parameters* 44 | # * [seconds: float] The time duration in seconds 45 | # * [always_show_hours: bool = `false`] Show hours even if the time is less than one hour 46 | # - - - - - - - - - - 47 | # *Returns* String 48 | # * The time duration as `hh:mm:ss` 49 | static func format_time_duration(second: float, always_show_hours = false) -> String: 50 | var h = floor(second / (60.0 * 60.0)) 51 | var m = floor((second - h * 60.0 * 60.0) / 60.0) 52 | var s = int(second) % 60 53 | var ret = "" 54 | if h > 0 or always_show_hours: 55 | if h < 10: ret += "0" 56 | ret += str(h, ":") 57 | if m < 10: ret += "0" 58 | ret += str(m, ":") 59 | if s < 10: ret += "0" 60 | ret += str(s) 61 | return ret 62 | 63 | -------------------------------------------------------------------------------- /utils/csv.gd: -------------------------------------------------------------------------------- 1 | # The csv module offers a number of high-level operations on csv files 2 | 3 | tool 4 | 5 | # Load tabel data from a csv file 6 | # The CSV must be encoded with UTF8 7 | # All empty line will be droped 8 | # - - - - - - - - - - 9 | # *Parameters* 10 | # * [`path`:`String`] The file path of the csv file 11 | # * [`delim`:`String`] The delimiter the csv file use. 12 | # - - - - - - - - - - 13 | # *Returns* `Array` 14 | # * An array contains all line of the csv data 15 | static func load_csv(path, delim=','): 16 | var array = [] 17 | var file = File.new() 18 | if OK == file.open(path, File.READ): 19 | while not file.eof_reached(): 20 | var line = file.get_csv_line(delim) 21 | var empty = true 22 | for column in line: 23 | empty = column.empty() and empty 24 | if line.size() > 0 and not empty: 25 | array.append(line) 26 | return array 27 | 28 | # Make dictionary from the loaded csv raw array data 29 | # This method will use the first row of the data as keys of other rows 30 | # - - - - - - - - - - 31 | # *Parameters* 32 | # * [`array`: Array] The csv data load from ``load_csv 33 | # - - - - - - - - - - 34 | # *Returns* `Array` 35 | # * Formated rows with key as first row 36 | static func csv2dict(array): 37 | var ret = [] 38 | if typeof(array) == TYPE_ARRAY: 39 | var keys = [] 40 | if array.size() > 0: 41 | keys = array[0] 42 | for i in range(1, array.size()): 43 | var line = array[i] 44 | if typeof(line) == TYPE_STRING_ARRAY and line.size() == keys.size(): 45 | var dictLine = {} 46 | for j in range(keys.size()): 47 | var key = keys[j] 48 | dictLine[key] = line[j] 49 | ret.append(dictLine) 50 | return ret 51 | -------------------------------------------------------------------------------- /utils/http.gd: -------------------------------------------------------------------------------- 1 | # The node to make http requests 2 | 3 | tool 4 | extends Node 5 | 6 | signal request_completed(url, headers, body) 7 | signal request_failed(url, result, response_code) 8 | signal request_progress(url, value, max_size) 9 | 10 | # url : HTTPRequest 11 | var request_nodes = {} 12 | 13 | # Start a http requestion 14 | # - - - - - - - - - - 15 | # *Parameters* 16 | # * [url: String] The url of the requestion 17 | # * [method: HTTPClient.Method ] The method of the requestion 18 | # * [headers: PoolStringArray ] Headers send to url for this request 19 | # * [body: String ] Datas of the request 20 | # - - - - - - - - - - 21 | # *Returns* Error 22 | # * Return OK or other error code 23 | func request(url, method = HTTPClient.METHOD_GET, headers=[], body = ""): 24 | var rq = HTTPRequest.new() 25 | # rq.name = url.replace("/", '\\') 26 | rq.use_threads = true 27 | rq.connect("request_completed", self, "_on_request_completed", [url]) 28 | add_child(rq) 29 | var ret = rq.request(url, headers, false, method, body) 30 | if OK == ret: 31 | request_nodes[url] = rq 32 | else: 33 | rq.queue_free() 34 | return ret 35 | 36 | # Cancel request for target url request 37 | # - - - - - - - - - - 38 | # *Parameters* 39 | # * [url: String] the url of the request 40 | func cancel_request(url): 41 | if url in request_nodes: 42 | var rq = request_nodes[url] 43 | rq.cancel_request() 44 | rq.queue_free() 45 | request_nodes.erase(url) 46 | 47 | # Resolve respose data 48 | # - - - - - - - - - - 49 | # *Parameters* 50 | # * [headers: PoolStringArray] response headers 51 | # * [body: PoolByteArray] raw response data 52 | # - - - - - - - - - - 53 | # *Returns* Variant 54 | # * Return the resolved result or the body if not resolved 55 | static func resolve_respose_body(headers, body): 56 | if typeof(body) == TYPE_RAW_ARRAY: 57 | var type = get_content_type(headers) 58 | match type: 59 | 'image/jpeg', 'application/x-jpg': 60 | var img = Image.new() 61 | img.load_jpg_from_buffer(body) 62 | return img 63 | 'image/png', 'application/x-png': 64 | var img = Image.new() 65 | img.load_png_from_buffer(body) 66 | return img 67 | 'image/webp': 68 | var img = Image.new() 69 | img.load_webp_from_buffer(body) 70 | return img 71 | 'application/json': 72 | return parse_json(body.get_string_from_utf8()) 73 | if type.begins_with("text/"): 74 | return body.get_string_from_utf8() 75 | return body 76 | 77 | # Get content type from the response headers 78 | # - - - - - - - - - - 79 | # *Parameters* 80 | # * [headers: PoolStringArray] headers 81 | # - - - - - - - - - - 82 | # *Returns* String 83 | # * The content type with lower case or an empty string if parse faild 84 | static func get_content_type(headers): 85 | var type = '' 86 | if typeof(headers) in [TYPE_STRING_ARRAY, TYPE_ARRAY]: 87 | for item in headers: 88 | item = item.to_lower() 89 | if item.begins_with('content-type:'): 90 | type = item.substr(len('content-type:'), len(item)).strip_edges() 91 | break 92 | return type 93 | 94 | 95 | func _process(delta): 96 | for url in request_nodes: 97 | var rq = request_nodes[url] 98 | if rq.get_http_client_status() == HTTPClient.STATUS_BODY: 99 | emit_signal('request_progress', url, rq.get_downloaded_bytes(), rq.get_body_size()) 100 | for node in get_children(): 101 | if not node in request_nodes.values(): 102 | node.queue_free() 103 | 104 | func _on_request_completed(result, response_code, headers, body, url): 105 | if not url in request_nodes: return 106 | var rq = request_nodes[url] 107 | if result == HTTPRequest.RESULT_SUCCESS and response_code == HTTPClient.RESPONSE_OK: 108 | emit_signal('request_progress', url, rq.get_downloaded_bytes(), rq.get_body_size()) 109 | emit_signal('request_completed', url, headers, body) 110 | else: 111 | emit_signal('request_failed', url, result, response_code) 112 | # remove the request node 113 | request_nodes[url].queue_free() 114 | request_nodes.erase(url) 115 | -------------------------------------------------------------------------------- /utils/json.gd: -------------------------------------------------------------------------------- 1 | # The json module offers a number of high-level operations on JSON data 2 | 3 | tool 4 | 5 | # Load json file instance 6 | # - - - - - - - - - - 7 | # *Parameters* 8 | # * [`json_path`:`String`] The file path of the json file 9 | # - - - - - - - - - - 10 | # *Returns* `Variant` 11 | # * A `Dictionary` instance for most situation 12 | static func load_json(json_path): 13 | var file = File.new() 14 | if OK == file.open(json_path, File.READ): 15 | return parse_json(file.get_as_text()) 16 | return {} 17 | 18 | # Save dictionary into json file 19 | # - - - - - - - - - - 20 | # *Parameters* 21 | # * [dict:Dictionary] The dictinary to save 22 | # * [path:String] The json file path to save as 23 | # - - - - - - - - - - 24 | # *Returns* Error 25 | # * Return OK if succeess 26 | static func save_json(dict, path): 27 | var err = OK 28 | if dict == null or typeof(dict) != TYPE_DICTIONARY or path == null or typeof(path) != TYPE_STRING: 29 | err = ERR_INVALID_PARAMETER 30 | if not Directory.new().dir_exists(path.get_base_dir()): 31 | Directory.new().make_dir_recursive(path.get_base_dir()) 32 | var f = File.new() 33 | err = f.open(path, File.WRITE) 34 | if OK == err: 35 | f.store_string(to_json(dict)) 36 | f.close() 37 | return OK 38 | else: 39 | return err 40 | 41 | # Get element value from a `Dictionary` safely 42 | # - - - - - - - - - - 43 | # *Parameters* 44 | # * [`ds`:`Dictionary`] The data source to query from 45 | # * [`key`: `Variant`] The key to query 46 | # * [`default`: `Variant`] The default value 47 | # - - - - - - - - - - 48 | # *Returns* `Variant` 49 | # * Get `key` from `ds`. 50 | # The `default` will be returned if `key` doesn't in the `ds`. 51 | static func get_element_value(ds, key, default=null): 52 | if typeof(ds) == TYPE_DICTIONARY and ds.has(key): 53 | return ds[key] 54 | return default 55 | 56 | # Query multi-properties form an dictionary 57 | # If the dictionary has not such elements default value will returned 58 | # - - - - - - - - - - 59 | # *Parameters* 60 | # * [ds:Dictionary] The data source to query from 61 | # * [format:Dictionary] The elements format with default value 62 | # - - - - - - - - - - 63 | # *Returns* Dictionary 64 | # * A dictionary with all keys in format 65 | # * If an elements in `format` doesn't exists in the `ds` the default value will be put into the result 66 | static func get_elements(ds, format= {}): 67 | if typeof(ds) == TYPE_DICTIONARY and typeof(ds) == TYPE_DICTIONARY: 68 | var res = {} 69 | for key in format: 70 | res[key] = get_element_value(ds, key, format[key]) 71 | return res 72 | else: 73 | return null 74 | 75 | # Dulicate a dictionary 76 | # - - - 77 | # **Parameters** 78 | # * dict: Dictionary The dictionary to duplicate 79 | # - - - 80 | # **Returns** 81 | # * Dictionary The duplicated dictionary instance 82 | static func duplicate(dict): 83 | if typeof(dict) == TYPE_DICTIONARY: 84 | return bytes2var(var2bytes(dict)) 85 | else: 86 | return dict 87 | 88 | enum { 89 | MERGE_OVERRIDE, # override exist data 90 | MERGE_KEEP # keep exist data 91 | } 92 | 93 | # Merge two dictionaries into one 94 | # None of the input parmameters would be changed 95 | # - - - 96 | # **Parameters** 97 | # * src_data: Dictionary The source dictionary to merge 98 | # * new_data: Dictionary The new dictionary that will be merge to `src_data` 99 | # * strategy: `MERGE_KEEP | MERGE_OVERRIDE` The merge strategy for both two dictionary have save key 100 | # * MERGE_KEEP : Keep values in `src_data` 101 | # * MERGE_OVERRIDE : Use values in `new_data` 102 | # - - - 103 | # **Returns** 104 | # * Dictionary The new merged dictionary 105 | static func merge(src_data, new_data, strategy = MERGE_OVERRIDE): 106 | if typeof(src_data) == TYPE_DICTIONARY and typeof(new_data) == TYPE_DICTIONARY: 107 | var ret = bytes2var(var2bytes(src_data)) 108 | for key in new_data: 109 | if not ret.has(key) or strategy == MERGE_OVERRIDE: 110 | ret[key] = new_data[key] 111 | return ret 112 | else: 113 | return src_data 114 | 115 | # Serialize script instance to a dictionary that could be save to json 116 | # - - - 117 | # **Parameters** 118 | # * inst: ScriptInstance The script instance to serialize with 119 | # - - - 120 | # **Returns** 121 | # * Dictionary The serialized dictionary of the instance 122 | static func serialize_instance(inst): 123 | var ret = inst 124 | if typeof(inst) == TYPE_OBJECT and inst.get_script() != null: 125 | var dict = inst2dict(inst) 126 | for key in dict: 127 | var prop = dict[key] 128 | dict[key] = serialize_instance(prop) 129 | ret = dict 130 | elif typeof(inst) == TYPE_ARRAY: 131 | ret = [] 132 | for ele in inst: 133 | ret.append(serialize_instance(ele)) 134 | elif typeof(inst) == TYPE_DICTIONARY: 135 | for key in inst: 136 | inst[key] = serialize_instance(inst[key]) 137 | return ret 138 | 139 | # Unserialize script instance from a dictionary that serialized with `serialize_instance` 140 | # - - - 141 | # **Parameters** 142 | # * dict: Dictionary The dictionary to unserialize with 143 | # - - - 144 | # **Returns** 145 | # * ScriptInstance The unserialized object instance 146 | static func unserialize_instance(dict): 147 | var ret = dict 148 | if typeof(dict) == TYPE_REAL and int(dict) == dict: 149 | ret = int(dict) 150 | elif typeof(dict) == TYPE_DICTIONARY: 151 | for key in dict: 152 | var prop = dict[key] 153 | dict[key] = unserialize_instance(prop) 154 | if dict.has("@path") and dict.has("@subpath"): 155 | ret = dict2inst(dict) 156 | elif typeof(dict) == TYPE_ARRAY: 157 | ret = [] 158 | for ele in dict: 159 | ret.append(unserialize_instance(ele)) 160 | return ret 161 | 162 | # Deep clone a script instance, a dictionary or an array 163 | # - - - 164 | # **Parameters** 165 | # * inst: The data to copy from 166 | # - - - 167 | # **Returns** 168 | # The same structured data cloned from inst 169 | static func deep_clone_instance(inst): 170 | var dict = serialize_instance(inst) 171 | var newdict = parse_json(to_json(dict)) 172 | return unserialize_instance(newdict) 173 | -------------------------------------------------------------------------------- /utils/utils.gd: -------------------------------------------------------------------------------- 1 | 2 | # Check does an object has all of script methods of target class 3 | # - - - - - - - - - - 4 | # *Parameters* 5 | # * [obj:Object] The object to check 6 | # * [interface:GDScript] The interface to check with 7 | # - - - - - - - - - - 8 | # *Returns* bool 9 | static func implements(obj: Object, interface:GDScript) -> bool: 10 | if obj == null or interface == null: 11 | return false 12 | if typeof(obj) != TYPE_OBJECT: 13 | return false 14 | if obj is interface: 15 | return true 16 | var interface_obj = interface.new() 17 | var required_methods = [] 18 | for m in interface_obj.get_method_list(): 19 | if m.flags & METHOD_FLAG_FROM_SCRIPT: 20 | required_methods.append(m.name) 21 | if not interface_obj is Reference: 22 | interface_obj.free() 23 | for mid in required_methods: 24 | if not obj.has_method(mid): 25 | return false 26 | return true 27 | 28 | # Format the time duration to a text to display 29 | # - - - - - - - - - - 30 | # *Parameters* 31 | # * [seconds: float] The time duration in seconds 32 | # * [always_show_hours: bool = `false`] Show hours even if the time is less than one hour 33 | # - - - - - - - - - - 34 | # *Returns* String 35 | # * The time duration as `hh:mm:ss` 36 | static func format_time_duration(second: float, always_show_hours = false) -> String: 37 | var h = floor(second / (60.0 * 60.0)) 38 | var m = floor((second - h * 60.0 * 60.0) / 60.0) 39 | var s = int(second) % 60 40 | var ret = "" 41 | if h > 0 or always_show_hours: 42 | if h < 10: ret += "0" 43 | ret += str(h, ":") 44 | if m < 10: ret += "0" 45 | ret += str(m, ":") 46 | if s < 10: ret += "0" 47 | ret += str(s) 48 | return ret 49 | -------------------------------------------------------------------------------- /utils/uuid.gd: -------------------------------------------------------------------------------- 1 | # The uuid module offers functions to generate unique id 2 | 3 | 4 | # MIT License 5 | 6 | # Copyright (c) 2017 Xavier Sellier 7 | 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | tool 26 | 27 | static func get_random_int(max_value): 28 | randomize() 29 | return randi() % max_value 30 | 31 | static func random_bytes(n): 32 | var r = [] 33 | for index in range(0, n): 34 | r.append(get_random_int(256)) 35 | return r 36 | 37 | static func uuidbin(): 38 | var b = random_bytes(16) 39 | 40 | b[6] = (b[6] & 0x0f) | 0x40 41 | b[8] = (b[8] & 0x3f) | 0x80 42 | return b 43 | 44 | static func v4(): 45 | var b = uuidbin() 46 | 47 | var low = '%02x%02x%02x%02x' % [b[0], b[1], b[2], b[3]] 48 | var mid = '%02x%02x' % [b[4], b[5]] 49 | var hi = '%02x%02x' % [b[6], b[7]] 50 | var clock = '%02x%02x' % [b[8], b[9]] 51 | var node = '%02x%02x%02x%02x%02x%02x' % [b[10], b[11], b[12], b[13], b[14], b[15]] 52 | 53 | return '%s-%s-%s-%s-%s' % [low, mid, hi, clock, node] 54 | --------------------------------------------------------------------------------