Phase 7: netfox + godot-jolt stack upgrade
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Contributors
|
||||
|
||||
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
|
||||
Studio], and the community. The following is the list of community contributors
|
||||
involved with netfox:
|
||||
|
||||
* Alberto Klocker <albertok@gmail.com>
|
||||
* Andrew Davis <jonandrewdavis@gmail.com>
|
||||
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
|
||||
* Dan Schuman <danschuman@gmail.com>
|
||||
* Dustie <77035922+DustieDog@users.noreply.github.com>
|
||||
* Eric Volpone <ericvolpone@gmail.com>
|
||||
* Gordon MacPherson <gordon@gordonite.tech>
|
||||
* Jake Cattrall <krazyjakee@gmail.com>
|
||||
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
|
||||
* Joseph Michael Ware <9at25jnr3@mozmail.com>
|
||||
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
|
||||
* Nicolas Batty <nicolas.batty@gmail.com>
|
||||
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
|
||||
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
|
||||
* Russ <622740+russ-@users.noreply.github.com>
|
||||
* Ryan Roden-Corrent <github@rcorre.net>
|
||||
* TheYellowArchitect <dmalandris@uth.gr>
|
||||
* TheYellowArchitect <hello@theyellowarchitect.com>
|
||||
* camperotactico <alberto.vgdd@gmail.com>
|
||||
* gk98s <89647115+gk98s@users.noreply.github.com>
|
||||
* zibetnu <9at25jnr3@mozmail.com>
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[Fox's Sake Studio]: https://github.com/foxssake/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright 2023 Gálffy Tamás
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
# netfox.internals
|
||||
|
||||
Shared utilities for [netfox] addons. Not intended for standalone usage.
|
||||
|
||||
Instead, check out the other addons in the [netfox] repository.
|
||||
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
@@ -0,0 +1,56 @@
|
||||
extends RefCounted
|
||||
class_name _BiMap
|
||||
|
||||
# Maps one-to-one associations in a bidirectional way
|
||||
|
||||
var _keys_to_values := {}
|
||||
var _values_to_keys := {}
|
||||
|
||||
func put(key: Variant, value: Variant) -> void:
|
||||
var old_value = _keys_to_values.get(key)
|
||||
var old_key = _values_to_keys.get(value)
|
||||
|
||||
if old_value != null: _values_to_keys.erase(old_value)
|
||||
if old_key != null: _keys_to_values.erase(old_key)
|
||||
|
||||
_keys_to_values[key] = value
|
||||
_values_to_keys[value] = key
|
||||
|
||||
func get_by_key(key: Variant) -> Variant:
|
||||
return _keys_to_values.get(key)
|
||||
|
||||
func get_by_value(value: Variant) -> Variant:
|
||||
return _values_to_keys.get(value)
|
||||
|
||||
func has_key(key: Variant) -> bool:
|
||||
return _keys_to_values.has(key)
|
||||
|
||||
func has_value(value: Variant) -> bool:
|
||||
return _values_to_keys.has(value)
|
||||
|
||||
func erase_key(key: Variant) -> bool:
|
||||
if not has_key(key):
|
||||
return false
|
||||
|
||||
var value = get_by_key(key)
|
||||
_values_to_keys.erase(value)
|
||||
_keys_to_values.erase(key)
|
||||
return true
|
||||
|
||||
func erase_value(value: Variant) -> bool:
|
||||
if not has_value(value):
|
||||
return false
|
||||
|
||||
var key = get_by_value(value)
|
||||
_values_to_keys.erase(value)
|
||||
_keys_to_values.erase(key)
|
||||
return true
|
||||
|
||||
func size() -> int:
|
||||
return _keys_to_values.size()
|
||||
|
||||
func keys() -> Array:
|
||||
return _keys_to_values.keys()
|
||||
|
||||
func values() -> Array:
|
||||
return _values_to_keys.keys()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b826s26d4ud5j
|
||||
@@ -0,0 +1,44 @@
|
||||
extends Object
|
||||
class_name _NetfoxEditorUtils
|
||||
|
||||
static func gather_properties(root: Node, callback_name: String, handler: Callable) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
|
||||
var nodes: Array[Node] = root.find_children("*")
|
||||
nodes.push_front(root)
|
||||
for node in nodes:
|
||||
if not node.has_method(callback_name):
|
||||
continue
|
||||
|
||||
var readable_node_name := "\"%s\" (\"%s\")" % [node.name, root.get_path_to(node)]
|
||||
if node.get(callback_name) == null:
|
||||
result.push_back("Can't grab method \"%s\" from node %s! Is it a @tool?" % [callback_name, readable_node_name])
|
||||
continue
|
||||
|
||||
var props = node.get(callback_name).call()
|
||||
if not props is Array:
|
||||
result.push_back("Node %s didn't return an array on calling \"%s\"" % [readable_node_name, callback_name])
|
||||
continue
|
||||
|
||||
for prop in props:
|
||||
if prop is String:
|
||||
# Property is a string, meaning property path relative to node
|
||||
handler.call(node, prop)
|
||||
elif prop is Array and prop.size() >= 2:
|
||||
# Property is a node-property tuple
|
||||
var prop_node: Node = null
|
||||
|
||||
# Node can be a String, NodePath, or an actual Node
|
||||
if prop[0] is String or prop[0] is NodePath:
|
||||
prop_node = node.get_node(prop[0])
|
||||
elif prop[0] is Node:
|
||||
prop_node = prop[0]
|
||||
else:
|
||||
result.push_back("Node %s specified invalid node in \"%s\": %s" % [readable_node_name, callback_name, prop])
|
||||
continue
|
||||
|
||||
handler.call(prop_node, prop[1])
|
||||
else:
|
||||
result.push_back("Node %s specified invalid property in \"%s\": %s" % [readable_node_name, callback_name, prop])
|
||||
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://0ohd4n5yjilb
|
||||
@@ -0,0 +1,70 @@
|
||||
extends RefCounted
|
||||
class_name _HistoryBuffer
|
||||
|
||||
# Maps ticks (int) to arbitrary data
|
||||
var _buffer: Dictionary = {}
|
||||
|
||||
func get_snapshot(tick: int):
|
||||
if _buffer.has(tick):
|
||||
return _buffer[tick]
|
||||
else:
|
||||
return null
|
||||
|
||||
func set_snapshot(tick: int, data):
|
||||
_buffer[tick] = data
|
||||
|
||||
func get_buffer() -> Dictionary:
|
||||
return _buffer
|
||||
|
||||
func get_earliest_tick() -> int:
|
||||
return _buffer.keys().min()
|
||||
|
||||
func get_latest_tick() -> int:
|
||||
return _buffer.keys().max()
|
||||
|
||||
func get_closest_tick(tick: int) -> int:
|
||||
if _buffer.has(tick):
|
||||
return tick
|
||||
|
||||
if _buffer.is_empty():
|
||||
return -1
|
||||
|
||||
var earliest_tick = get_earliest_tick()
|
||||
if tick < earliest_tick:
|
||||
return earliest_tick
|
||||
|
||||
var latest_tick = get_latest_tick()
|
||||
if tick > latest_tick:
|
||||
return latest_tick
|
||||
|
||||
return _buffer.keys() \
|
||||
.filter(func (key): return key < tick) \
|
||||
.max()
|
||||
|
||||
func get_history(tick: int):
|
||||
var closest_tick = get_closest_tick(tick)
|
||||
return _buffer.get(closest_tick)
|
||||
|
||||
func trim(earliest_tick_to_keep: int):
|
||||
var ticks := _buffer.keys()
|
||||
for tick in ticks:
|
||||
if tick < earliest_tick_to_keep:
|
||||
_buffer.erase(tick)
|
||||
|
||||
func clear():
|
||||
_buffer.clear()
|
||||
|
||||
func size() -> int:
|
||||
return _buffer.size()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _buffer.is_empty()
|
||||
|
||||
func has(tick) -> bool:
|
||||
return _buffer.has(tick)
|
||||
|
||||
func ticks() -> Array:
|
||||
return _buffer.keys()
|
||||
|
||||
func erase(tick):
|
||||
_buffer.erase(tick)
|
||||
@@ -0,0 +1 @@
|
||||
uid://btuvvlw3jkx2e
|
||||
@@ -0,0 +1,208 @@
|
||||
extends RefCounted
|
||||
class_name NetfoxLogger
|
||||
|
||||
## Logger implementation for use with netfox
|
||||
##
|
||||
## NetfoxLoggers implement distinct log levels. These can be used to filter
|
||||
## which messages are actually emitted. All messages are output using [method
|
||||
## @GlobalScope.print]. Warnings and errors are also pushed to the debug panel,
|
||||
## using [method @GlobalScope.push_warning] and [method @GlobalScope.push_error]
|
||||
## respectively, if [member push_to_debugger] is enabled.
|
||||
## [br][br]
|
||||
## Every logger has a name, and belongs to a module. Logging level can be
|
||||
## overridden per module, using [member module_log_level].
|
||||
## [br][br]
|
||||
## Loggers also support tags. Tags can be used to provide extra pieces of
|
||||
## information that are logged with each message. Some tags are provided by
|
||||
## netfox. Additional tags can be added using [method register_tag].
|
||||
##
|
||||
## @tutorial(Logging Guide): https://foxssake.github.io/netfox/latest/netfox/guides/logging/
|
||||
|
||||
|
||||
enum {
|
||||
LOG_ALL, ## Filter level to log every message
|
||||
LOG_TRACE, ## Trace logs, the most verbose level
|
||||
LOG_DEBUG, ## Debug logs
|
||||
LOG_INFO, ## Info logs
|
||||
LOG_WARN, ## Warnings
|
||||
LOG_ERROR, ## Errors
|
||||
LOG_NONE ## Filter level to log no messages
|
||||
}
|
||||
|
||||
## Default log level to fall back on, if not configured
|
||||
const DEFAULT_LOG_LEVEL := LOG_DEBUG
|
||||
|
||||
const _LEVEL_PREFIXES: Array[String] = [
|
||||
"",
|
||||
"TRC",
|
||||
"DBG",
|
||||
"INF",
|
||||
"WRN",
|
||||
"ERR",
|
||||
""
|
||||
]
|
||||
|
||||
## Global logging level, used by all loggers
|
||||
static var log_level: int
|
||||
|
||||
## Per-module logging level, used only by loggers belonging to the given module
|
||||
## [br][br]
|
||||
## This is a dictionary that associates module names ( strings ) to log levels
|
||||
## ( int, e.g. [constant LOG_DEBUG] ).
|
||||
static var module_log_level: Dictionary
|
||||
|
||||
## Set to true to enable calling [@GlobalScope.push_warning] and
|
||||
## [@GlobalScope.push_error]
|
||||
static var push_to_debugger := true
|
||||
|
||||
static var _tags: Dictionary = {}
|
||||
static var _ordered_tags: Array[Callable] = []
|
||||
|
||||
## Logger module
|
||||
var module: String
|
||||
## Logger name
|
||||
var name: String
|
||||
|
||||
|
||||
## Register a tag
|
||||
## [br][br]
|
||||
## Tags are callables that provide pieces of context, included in all log
|
||||
## messges. The [param tag] callable must return a string.
|
||||
static func register_tag(tag: Callable, priority: int = 0) -> void:
|
||||
# Save tag
|
||||
if not _tags.has(priority):
|
||||
_tags[priority] = [tag]
|
||||
else:
|
||||
_tags[priority].push_back(tag)
|
||||
|
||||
# Recalculate tag order
|
||||
_ordered_tags.clear()
|
||||
|
||||
var prio_groups = _tags.keys()
|
||||
prio_groups.sort()
|
||||
|
||||
for prio_group in prio_groups:
|
||||
var tag_group = _tags[prio_group]
|
||||
_ordered_tags.append_array(tag_group)
|
||||
|
||||
## Free an already registered tag
|
||||
static func free_tag(tag: Callable) -> void:
|
||||
for priority in _tags.keys():
|
||||
var priority_group := _tags[priority] as Array
|
||||
priority_group.erase(tag)
|
||||
|
||||
# NOTE: Arrays are passed as reference, no need to re-assign after modifying
|
||||
if priority_group.is_empty():
|
||||
_tags.erase(priority)
|
||||
|
||||
_ordered_tags.erase(tag)
|
||||
|
||||
static func _static_init():
|
||||
log_level = ProjectSettings.get_setting(&"netfox/logging/log_level", DEFAULT_LOG_LEVEL)
|
||||
module_log_level = {
|
||||
"netfox": ProjectSettings.get_setting(&"netfox/logging/netfox_log_level", DEFAULT_LOG_LEVEL),
|
||||
"netfox.noray": ProjectSettings.get_setting(&"netfox/logging/netfox_noray_log_level", DEFAULT_LOG_LEVEL),
|
||||
"netfox.extras": ProjectSettings.get_setting(&"netfox/logging/netfox_extras_log_level", DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
|
||||
static func _for_netfox(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox", p_name)
|
||||
|
||||
static func _for_noray(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox.noray", p_name)
|
||||
|
||||
static func _for_extras(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox.extras", p_name)
|
||||
|
||||
static func _make_setting(name: String) -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"value": DEFAULT_LOG_LEVEL,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_ENUM,
|
||||
"hint_string": "All,Trace,Debug,Info,Warning,Error,None"
|
||||
}
|
||||
|
||||
|
||||
func _init(p_module: String, p_name: String):
|
||||
module = p_module
|
||||
name = p_name
|
||||
|
||||
## Log a trace message
|
||||
## [br][br]
|
||||
## Traces are the most verbose, usually used for drilling down into very niche
|
||||
## bugs.
|
||||
func trace(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_TRACE)
|
||||
|
||||
## Log a debug message
|
||||
## [br][br]
|
||||
## Debug messages are verbose, usually used to reconstruct and investigate bugs.
|
||||
func debug(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_DEBUG)
|
||||
|
||||
## Log an info message
|
||||
## [br][br]
|
||||
## Info messages provide general notifications about application events.
|
||||
func info(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_INFO)
|
||||
|
||||
## Log a warning message
|
||||
## [br][br]
|
||||
## This is also forwarded to [method @GlobalScope.push_warning], if enabled with
|
||||
## [member push_to_debugger]. Warning messages usually indicate that something
|
||||
## has gone wrong, but is recoverable.
|
||||
func warning(text: String, values: Array = []):
|
||||
if _check_log_level(LOG_WARN):
|
||||
var formatted_text = _format_text(text, values, LOG_WARN)
|
||||
if push_to_debugger:
|
||||
push_warning(formatted_text)
|
||||
|
||||
# Print so it shows up in the Output panel too
|
||||
print(formatted_text)
|
||||
|
||||
## Log an error message
|
||||
## [br][br]
|
||||
## This is also forwarded to [method @GlobalScope.push_error], if enabled with
|
||||
## [member push_to_debugger]. Error messages usually indicate an issue that
|
||||
## can't be recovered from.
|
||||
func error(text: String, values: Array = []):
|
||||
if _check_log_level(LOG_ERROR):
|
||||
var formatted_text = _format_text(text, values, LOG_ERROR)
|
||||
if push_to_debugger:
|
||||
push_error(formatted_text)
|
||||
|
||||
# Print so it shows up in the Output panel too
|
||||
print(formatted_text)
|
||||
|
||||
func _check_log_level(level: int) -> bool:
|
||||
var cmp_level = log_level
|
||||
if level < cmp_level:
|
||||
return false
|
||||
|
||||
if module_log_level.has(module):
|
||||
var module_level = module_log_level.get(module)
|
||||
return level >= module_level
|
||||
|
||||
return true
|
||||
|
||||
func _format_text(text: String, values: Array, level: int) -> String:
|
||||
level = clampi(level, LOG_TRACE, LOG_ERROR)
|
||||
|
||||
var result := PackedStringArray()
|
||||
|
||||
result.append("[%s]" % [_LEVEL_PREFIXES[level]])
|
||||
for tag in _ordered_tags:
|
||||
result.append("[%s]" % [tag.call()])
|
||||
result.append("[%s::%s] " % [module, name])
|
||||
|
||||
if values.is_empty():
|
||||
result.append(text)
|
||||
else:
|
||||
result.append(text % values)
|
||||
|
||||
return "".join(result)
|
||||
|
||||
func _log_text(text: String, values: Array, level: int):
|
||||
if _check_log_level(level):
|
||||
print(_format_text(text, values, level))
|
||||
@@ -0,0 +1 @@
|
||||
uid://dp4rakv0drj1f
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="netfox.internals"
|
||||
description="Shared internals for netfox addons"
|
||||
author="Tamas Galffy and contributors"
|
||||
version="1.35.3"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,34 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
var SETTINGS = [
|
||||
NetfoxLogger._make_setting("netfox/logging/log_level")
|
||||
]
|
||||
|
||||
func _enter_tree():
|
||||
for setting in SETTINGS:
|
||||
add_setting(setting)
|
||||
|
||||
func _exit_tree():
|
||||
if ProjectSettings.get_setting("netfox/general/clear_settings", false):
|
||||
for setting in SETTINGS:
|
||||
remove_setting(setting)
|
||||
|
||||
func add_setting(setting: Dictionary):
|
||||
if ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.set_setting(setting.name, setting.value)
|
||||
ProjectSettings.set_initial_value(setting.name, setting.value)
|
||||
ProjectSettings.add_property_info({
|
||||
"name": setting.get("name"),
|
||||
"type": setting.get("type"),
|
||||
"hint": setting.get("hint", PROPERTY_HINT_NONE),
|
||||
"hint_string": setting.get("hint_string", "")
|
||||
})
|
||||
|
||||
func remove_setting(setting: Dictionary):
|
||||
if not ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.clear(setting.name)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvhj7f66i6yl4
|
||||
@@ -0,0 +1,34 @@
|
||||
extends RefCounted
|
||||
class_name _RingBuffer
|
||||
|
||||
var _data: Array
|
||||
var _capacity: int
|
||||
var _size: int = 0
|
||||
var _head: int = 0
|
||||
|
||||
func _init(p_capacity: int):
|
||||
_capacity = p_capacity
|
||||
_data = []
|
||||
_data.resize(p_capacity)
|
||||
|
||||
func push(item):
|
||||
_data[_head] = item
|
||||
|
||||
_size += 1
|
||||
_head = (_head + 1) % _capacity
|
||||
|
||||
func get_data() -> Array:
|
||||
if _size < _capacity:
|
||||
return _data.slice(0, _size)
|
||||
else:
|
||||
return _data
|
||||
|
||||
func size() -> int:
|
||||
return _size
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _size == 0
|
||||
|
||||
func clear():
|
||||
_size = 0
|
||||
_head = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmfbs3cx33lkw
|
||||
@@ -0,0 +1,64 @@
|
||||
extends RefCounted
|
||||
class_name _Set
|
||||
|
||||
var _data: Dictionary = {}
|
||||
var _iterator_idx: int = -1
|
||||
|
||||
static func of(items: Array) -> _Set:
|
||||
var result := _Set.new()
|
||||
for item in items:
|
||||
result.add(item)
|
||||
return result
|
||||
|
||||
func add(value):
|
||||
_data[value] = true
|
||||
|
||||
func has(value) -> bool:
|
||||
return _data.has(value)
|
||||
|
||||
func size() -> int:
|
||||
return _data.size()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _data.is_empty()
|
||||
|
||||
func erase(value):
|
||||
return _data.erase(value)
|
||||
|
||||
func clear():
|
||||
_data.clear()
|
||||
|
||||
func values() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
func min():
|
||||
return _data.keys().min()
|
||||
|
||||
func max():
|
||||
return _data.keys().max()
|
||||
|
||||
func equals(other) -> bool:
|
||||
if not other or not other is _Set:
|
||||
return false
|
||||
|
||||
return values() == other.values()
|
||||
|
||||
func _to_string():
|
||||
return "Set" + str(values())
|
||||
|
||||
func _iter_init(arg) -> bool:
|
||||
_iterator_idx = 0
|
||||
return _can_iterate()
|
||||
|
||||
func _iter_next(arg) -> bool:
|
||||
_iterator_idx += 1
|
||||
return _can_iterate()
|
||||
|
||||
func _iter_get(arg):
|
||||
return _data.keys()[_iterator_idx]
|
||||
|
||||
func _can_iterate() -> bool:
|
||||
if _data.is_empty() or _iterator_idx >= _data.size():
|
||||
_iterator_idx = -1
|
||||
return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjvmp7eed1aj0
|
||||
Reference in New Issue
Block a user