Skip to content

Instantly share code, notes, and snippets.

@Andre-LA
Last active October 31, 2025 22:47
Show Gist options
  • Select an option

  • Save Andre-LA/800b39c74cb5c83bf640f0776e9c4b40 to your computer and use it in GitHub Desktop.

Select an option

Save Andre-LA/800b39c74cb5c83bf640f0776e9c4b40 to your computer and use it in GitHub Desktop.
Python script to dump teal symbols. Note this is a fork of o3de's dump_lua_symbols.py
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
# NOTE: (the instructions below is for dump_lua_symbols.py, just use pyRunFile with dump_teal_symbols.py)
# The easiest way to run this script in O3DE
# is to run it via the "Python Scripts" window in the Editor:
# Tools -> Other -> Python Scripts.
# Will produce the file <game_project>\lua_symbols.txt
#
# Alternatively from the Editor console:
# pyRunFile C:\GIT\o3de\Gems\EditorPythonBindings\Editor\Scripts\dump_lua_symbols.py
# Will produce the file <game_project>\lua_symbols.txt
#
# or if you need to customize the name of the output file
# pyRunFile C:\GIT\o3de\Gems\EditorPythonBindings\Editor\Scripts\dump_lua_symbols.py -o my_lua_name.txt
# Will produce the file <game_project>\my_lua_name.txt
# This script shows basic usage of the LuaSymbolsReporterBus,
# Which can be used to report all symbols available for
# game scripting with Lua.
import sys
import os
import argparse
import re
from typing import TextIO
import azlmbr.bus as azbus
import azlmbr.script as azscript
import azlmbr.legacy.general as azgeneral
re_return_type = re.compile("^\[\=([\w_\.]+)\]")
re_return_and_args = re.compile("^(?:\[\=([\w_.]+)\] )?([\w_, .]+)")
re_args = re.compile("([\w_.]+)[, ]?")
re_args_checker = re.compile("(([\w_.]+, )+([\w_.]+)?)|[\w_.]+")
re_symbol_checker = re.compile("[\w_]+")
arg_separator = ", "
types_map = {
"any": "any",
"void": "any",
"...": "any...",
"char": "string",
"bool": "boolean",
"float": "number",
"double": "number",
"int": "integer",
"short": "integer",
"long": "integer",
"unsignedchar": "integer",
"unsignedint": "integer",
"unsignedlong": "integer",
"unsignedshort": "integer",
}
unknown_types = []
known_types = []
start_content = """
global interface LuaComponent
entityId: EntityId
OnActivate: function(self)
OnDeactivate: function(self)
end
global interface EBusHandler<T>
Disconnect: function(self)
end
local interface BusThatHasAHandler<T> -- TODO: think of better name
Connect: function(table, ...: any): EBusHandler<T>
end
"""
# TODO: I don't like manual work, but for now, let's use it
# later the C++ bit probably could help us here...
metamethods_map: dict[str, list[str]] = {
"Vector3": [
"__call: function(self, number): Vector3",
"__call: function(self, number, number, number): Vector3",
"__mul: function(Vector3, Vector3): Vector3",
"__mul: function(Vector3, number): Vector3",
]
}
# TODO: stop localizing fields
# TODO: use list[type], and try to type everything
def to_teal_type(t: str):
x = types_map.get(t)
if x != None:
return x
if t not in unknown_types:
unknown_types.append(t)
return t
def ebus_has_broadcast(ebus_symbol:azlmbr.script.LuaEBusSymbol) -> bool:
return ebus_symbol.canBroadcast
def ebus_has_events(ebus_symbol:azlmbr.script.LuaEBusSymbol) -> bool:
for sender in ebus_symbol.senders:
if sender.category == "Event":
return True
return False
def ebus_has_notifications(ebus_symbol:azlmbr.script.LuaEBusSymbol) -> bool:
for sender in ebus_symbol.senders:
if sender.category == "Notification":
return True
return False
def _dump_teal_property(result, prop_sym, prefix: str):
result.append(f"{prefix}{prop_sym.name}: any\n")
def _dump_teal_args(result, args_info: str, try_find_self_arg: bool):
return_and_args_match = re_return_and_args.match(args_info)
if return_and_args_match == None:
return
args_content = return_and_args_match.group(2)
has_args = re_args_checker.fullmatch(args_content)
if has_args == None:
return
first_arg_is_self: bool = False
args_array = []
for arg in re_args.finditer(args_content):
args_array.append(arg.group(1))
if try_find_self_arg and len(args_array) >= 2:
if args_array[0] == "void" and args_array[1] == "void":
first_arg_is_self = True
if first_arg_is_self:
args_array.pop(0)
args_array[0] = "self"
args_array = map(to_teal_type, args_array)
args_result = arg_separator.join(args_array)
result.append(args_result)
def _dump_teal_function(result, fun_sym, prefix: str):
return_type_match = re_return_type.match(fun_sym.debugArgumentInfo)
return_type = "any"
if return_type_match != None:
return_type = return_type_match.group(1)
result.append(f"{prefix}{fun_sym.name}: function(")
_dump_teal_args(result, fun_sym.debugArgumentInfo, False)
result.append(f"): {to_teal_type(return_type)}\n")
def _dump_teal_class_symbol(result, class_symbol: azlmbr.script.LuaClassSymbol):
has_valid_class_name = re_symbol_checker.fullmatch(class_symbol.name)
if has_valid_class_name == None:
return
if class_symbol.name not in known_types:
known_types.append(class_symbol.name)
result.append(f"global record {class_symbol.name}\n")
# TODO: sort properties
for property_symbol in class_symbol.properties:
_dump_teal_property(result, property_symbol, "\t")
if len(class_symbol.properties) > 0:
result.append("\n")
# TODO: sort method
for method_symbol in class_symbol.methods:
_dump_teal_function(result, method_symbol, "\t")
metamethods = metamethods_map.get(class_symbol.name)
if metamethods != None:
result.append("\n")
for metamethod in metamethods:
result.append(f"\tmetamethod {metamethod}\n")
result.append("end\n")
def _dump_teal_classes(result):
class_symbols = azscript.LuaSymbolsReporterBus(
azbus.Broadcast, "GetListOfClasses"
)
result.append("--[[ ======== Classes ========== ]]--\n")
sorted_classes_by_named = sorted(class_symbols, key=lambda class_symbol: class_symbol.name)
for class_symbol in sorted_classes_by_named:
_dump_teal_class_symbol(result, class_symbol)
result.append("\n\n")
def _dump_teal_globals(result):
# dump global properties
global_properties = azscript.LuaSymbolsReporterBus(
azbus.Broadcast, "GetListOfGlobalProperties"
)
result.append("--[[ ======== Global Properties ========== ]]--\n")
sorted_properties_by_name = sorted(global_properties, key = lambda symbol: symbol.name)
for property_symbol in sorted_properties_by_name:
_dump_teal_property(result, property_symbol, "global ")
result.append("\n\n")
# dump global functions
global_functions = azscript.LuaSymbolsReporterBus(
azbus.Broadcast, "GetListOfGlobalFunctions"
)
result.append("--[[ ======== Global Functions ========== ]]--\n")
sorted_functions_by_name = sorted(global_functions, key=lambda symbol: symbol.name)
for function_symbol in sorted_functions_by_name:
_dump_teal_function(result, function_symbol, "global ")
result.append("\n\n")
def _dump_teal_ebus_sender(result, ebus_sender: azlmbr.script.LuaEBusSender, prefix: str):
result.append(f"{prefix}{ebus_sender.name}: function(")
_dump_teal_args(result, ebus_sender.debugArgumentInfo, True)
result.append("): any\n")
def _dump_teal_ebus_senders(result, ebus_senders, category: str, prefix: str):
for sender in ebus_senders:
if sender.category == category:
_dump_teal_ebus_sender(result, sender, prefix)
def _dump_teal_ebus(result, ebus_symbol: azlmbr.script.LuaEBusSymbol):
has_valid_class_name = re_symbol_checker.fullmatch(ebus_symbol.name)
if has_valid_class_name == None:
return
sorted_senders = sorted(ebus_symbol.senders, key=lambda symbol: symbol.name)
result.append(f"global record {ebus_symbol.name}\n")
if ebus_symbol.hasHandler:
result.append(f"\tis BusThatHasAHandler<{ebus_symbol.name}>\n\n")
if ebus_has_events(ebus_symbol):
result.append("\trecord Event\n")
_dump_teal_ebus_senders(result, sorted_senders, "Event", "\t\t")
result.append("\tend\n\n")
if ebus_has_broadcast(ebus_symbol):
result.append("\trecord Broadcast\n")
_dump_teal_ebus_senders(result, sorted_senders, "Broadcast", "\t\t")
result.append("\tend\n")
if ebus_has_notifications(ebus_symbol):
result.append("\tinterface Notification\n")
_dump_teal_ebus_senders(result, sorted_senders, "Notification", "\t\t")
result.append("\tend\n")
result.append("end\n")
result.append("\n\n")
def _dump_teal_ebuses(result):
ebuses = azscript.LuaSymbolsReporterBus(
azbus.Broadcast, "GetListOfEBuses"
)
result.append("--[[ ======== Ebus List ========== ]]\n")
sorted_ebuses_by_name = sorted(ebuses, key=lambda symbol: symbol.name)
for ebus_symbol in sorted_ebuses_by_name:
_dump_teal_ebus(result, ebus_symbol)
result.append("\n\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Dumps All ebuses, classes and global symbols available for Lua scripting.')
parser.add_argument('--outfile', '--o', default='o3de.d.tl',
help='output file file where all the symbols will be dumped to. If relative, will be under the game project folder.')
parser.add_argument('--all', '--a', default=True, action='store_true',
help='If true dumps all symbols to the outfile. Equivalent to specifying --c --g --e')
parser.add_argument('--classes', '--c', default=False, action='store_true',
help='If true dumps Class symbols.')
parser.add_argument('--globals', '--g', default=False, action='store_true',
help='If true dumps Global symbols.')
parser.add_argument('--ebuses', '--e', default=False, action='store_true',
help='If true dumps Ebus symbols.')
args = parser.parse_args()
output_file_name = args.outfile
if not os.path.isabs(output_file_name):
game_root_path = os.path.normpath(azgeneral.get_game_folder())
output_file_name = os.path.join(game_root_path, output_file_name)
try:
file_obj = open(output_file_name, 'wt')
except Exception as e:
print(f"Failed to open {output_file_name}: {e}")
sys.exit(-1)
result = []
if args.classes:
_dump_teal_classes(result)
if args.globals:
_dump_teal_globals(result)
if args.ebuses:
_dump_teal_ebuses(result)
if (not args.classes) and (not args.globals) and (not args.ebuses):
_dump_teal_classes(result)
_dump_teal_globals(result)
_dump_teal_ebuses(result)
file_obj.write(start_content)
file_obj.write("\n\n")
for known_type in known_types:
if known_type in unknown_types:
unknown_types.remove(known_type)
for unknown_type in unknown_types:
file_obj.write(f"global record {unknown_type} end\n")
file_obj.write("\n\n")
for result_line in result:
file_obj.write(result_line)
file_obj.close()
print(f" Teal Symbols Are available in: {output_file_name}")
@Andre-LA
Copy link
Author

Andre-LA commented Oct 31, 2025

Updated, now we can do things like this:

local record MyComponent
	-- LuaComponent has OnActivate, entityId, etc (WIP)
	is LuaComponent, TickBus.Notification -- every Bus with Notifications has a Notification interface, with the notifications callbacks inside it.


	record PropertiesType
		Speed: number
		Direction: Vector3
	end

	Properties: PropertiesType

	-- handler of a specific Bus, thus a typed handler
	tickHandler: EBusHandler<TickBus>
end

local NewComponent: MyComponent = 
{
	Properties =
	{
		-- Property definitions
		Speed = 10,
		Direction = Vector3.CreateAxisZ(1)
	}
}

-- OnActivate works because our component "is" a "LuaComponent"
function NewComponent:OnActivate()
	-- TickBus is a "BusThatHasAHandler"... yeah I need to think of a better name :)
	self.tickHandler = TickBus.Connect(self as table) -- returns EBusHandler<TickBus>!
end

function NewComponent:OnDeactivate()
	self.tickHandler:Disconnect()
end

-- Since our component "is" also a "TickBus.Notification", teal will understand
-- that it has a OnTick callback.
function NewComponent:OnTick(dt: number, time: ScriptTimePoint): any -- this `: any` is a current limitation, every notification for now returns an any

	local delta = Vector3(0) -- __call isn't automatic unfortately, check metamethods_map
	delta = self.Properties.Direction * self.Properties.Speed -- same
	TransformBus.Event.MoveEntity(self.entityId, delta)
end

return NewComponent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment