Initial commit.

This commit is contained in:
Robert MacGregor 2016-01-25 19:18:06 -05:00
commit a587d317af
4 changed files with 633 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.c
*.pyc
out.txt

19
LICENSE.txt Normal file
View file

@ -0,0 +1,19 @@
The MIT License (MIT)
Copyright (c) 2016 Robert MacGregor
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.

464
scraper.py Normal file
View file

@ -0,0 +1,464 @@
"""
scraper.py
The scraper class is used to scrape data from a decompiled form of the
Tribes 2 game executable in order to build a tree of sorts that can be
used for mapping out the various functions, global variables and
datablock types & their associated properties.
This software is licensed under the MIT license. Refer to LICENSE.txt for
details.
Copyright (c) 2016 Robert MacGregor
"""
import re
import string
class EngineComponent(object):
"""
The base representation type for all the data the scraper will be
pulling from the pseudo source code.
"""
name = None
address = None
type_name = None
description = None
def __init__(self, name, address, type_name, description):
self.name = name
self.address = address
self.type_name = type_name
self.description = description
class Function(EngineComponent):
"""
The virtual representation of a callable engine function from Torque
Script. It contains a description, the address, argument information
and if applicable, the object typename it is bound to.
"""
min_args = None
max_args = None
def __init__(self, name, address, type_name, description, min_args, max_args):
EngineComponent.__init__(self, name, address, type_name, description)
self.min_args = min_args
self.max_args = max_args
class GlobalVariable(EngineComponent):
def __init__(self, name, address, type_name):
EngineComponent.__init__(self, name, address, type_name, None)
class Datablock(EngineComponent):
"""
The virtual representation of the Torque Game Engine datablock used
for synchronization of custom simulation parameters across the network.
"""
properties = None
class Property(EngineComponent):
def __init__(self, name, address, type_name):
EngineComponent.__init__(self, name, address, type_name, None)
def __init__(self, name):
EngineComponent.__init__(self, name, None, None, None)
self.properties = { }
class Scraper(object):
"""
The meat and potatoes of the scraper system. This is your primary
class to instantiate
"""
_global_function_registry = ["426650", "426590", "4265D0", "426550", "426610"]
"""
The global function registry is used by the scraper to determine prefixes
of all the sub routines that register global functions in the Tribes 2
engine. They take the following format: sub_######
"""
_type_function_registry = ["426450", "426510", "426450", "425960"]
"""
The type function registry is the same as the global function registry,
except used for type contextual functions. The registration signatures
for these functions are slightly different.
"""
_datablock_property_registry = ["423F20"]
"""
Registration subroutines regarding static fields of datablocks.
"""
_global_value_registry = ["4263B0"]
"""
Registration subroutines regarding globally addressible variables.
"""
_registration_expression_template = "sub_(%s)[^;{]+;[^\"]"
"""
Base regular expression used for matching the various registration
calls. It is formatted with the above values in-place for operating
within the different contexts.
"""
_datablock_type_table = {
"61E7A0": "ExplosionData",
"5B4F60": "WaterBlockData",
"612400": "WheeledVehicleData",
"6161E0": "HoverVehicleData",
"5CE810": "PlayerData",
"6034C0": "ItemData",
"69C170": "TriggerData",
"50DC70": "AudioProfileData",
"62B3C0": "LinearProjectile",
"60F820": "FlyingVehicleData",
"6370D0": "SeekingProjectileData",
"69B0F0": "PrecipitationData",
"641480": "SniperProjectileData",
"66A270": "SensorData",
"6303F0": "GrenadeProjectileData",
"6333D0": "GrenadeProjectileData",
"694B40": "TracerProjectileData",
"6470D0": "TargetProjectileData",
"653E10": "TurretData",
"654AE0": "TurretData",
"5E4C20": "TurretData", # Camera?
"654330": "TurretImageData", # TurretData?
"64E2B0": "LightningData",
"627150": "LightningData",
"621DF0": "ParticleEmitterData",
"622E60": "ParticleData",
"644910": "ELFProjectileData",
"64A860": "ELFProjectileData",
"5F4D90": "ShapeBaseImageData",
"602940": "StaticShapeData",
"66B000": "SpawnSphere",
"6099E0": "VehicleData",
"47D880": "AI Task?",
"63D870": "LinearFlareData",
"59A870": "TerrainData",
"68C4B0": "ShockwaveData",
"4B5840": "CorpseData",
"619B30": "Sky",
"5AB310": "Sky",
"68AAA0": "PhysicalZone",
"626240": "Debris",
"684000": "Debris",
"6751A0": "ForceFieldBareData",
"631A50": "ProjectileData", # Base?
"69AF10": "FireballAtmosphere",
}
"""
The datablock type table is used when looking up the context of a given static datablock field registration
call. This context is merely the address of the calling subroutine, so multiple entries may have to be added
for a single datablock type.
"""
# Hacks
string_expression = re.compile("\" *\S+\" *")
# Global method material
global_method_add_expression = re.compile(_registration_expression_template % string.join(_global_function_registry, "|"), re.IGNORECASE)
type_method_add_expression = re.compile(_registration_expression_template % string.join(_type_function_registry, "|"), re.IGNORECASE)
datablock_property_add_expression = re.compile(_registration_expression_template % string.join(_datablock_property_registry, "|"), re.IGNORECASE)
global_value_add_expression = re.compile(_registration_expression_template % string.join(_global_value_registry, "|"), re.IGNORECASE)
type_function_total = 0
global_function_count = 0
type_function_counts = None
primitive_type_mapping = [
"Unknown",
"Integer",
"Unknown",
"Boolean",
"Unknown",
"Float",
"Unknown"
]
# Dictionary containing type name to inheritance list mappings
type_name_inheritance = {
"HTTPObject": ["HTTPObject", "TCPObject", "SimObject"],
"FileObject": ["FileObject", "SimObject"],
"Item": ["Item", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
"SceneObject": ["SceneObject", "NetObject", "SimObject"],
"Player": ["Player", "Player", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
"DebugView": ["DebugView", "GuiTextCtrl", "GuiControl", "SimGroup", "SimSet", "SimObject"],
"GameBase": ["GameBase", "SceneObject", "NetObject", "SimObject"],
"SimpleNetObject": ["SimpleNetObject", "SimObject"],
"SimObject": ["SimObject"],
"Canvas": ["Canvas", "GuiCanvas", "GuiControl", "SimGroup", "SimSet", "SimObject"],
"GuiCanvas": ["GuiCanvas", "GuiControl", "SimGroup", "SimSet", "SimObject"],
"AIObjectiveQ": ["AIObjectiveQ", "SimSet", "SimObject"],
"ForceFieldBare": ["ForceFieldBare", "GameBase", "SceneObject", "NetObject", "SimObject"],
"PhysicalZone": ["PhysicalZone", "SceneObject", "NetObject", "SimObject" ],
"AIConnection": ["AIConnection", "GameConnection", "GameConnection", "GameConnection", "NetConnection", "SimGroup", "SimSet", "SimObject"],
"Turret": ["Turret", "StaticShape", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject" ],
"TerrainBlock": ["TerrainBlock", "SceneObject", "NetObject", "SimObject"],
"PlayerData": ["PlayerData", "ShapeBaseData", "GameBaseData", "SimDataBlock", "SimObject"],
"InheriorInstance": ["InteriorInstance", "SceneObject", "NetObject", "SimObject"],
"StaticShape": ["StaticShape", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
"Trigger": ["Trigger", "GameBase", "SceneObject", "NetObject", "SimObject"],
"WaterBlock": ["WaterBlock", "SceneObject", "NetObject", "SimObject"],
"FireballAtmosphere": ["FireballAtmosphere", "GameBase", "SceneObject", "NetObject", "SimObject"],
"MissionArea": ["MissionArea", "NetObject", "SimObject"],
"TSStatic": ["TSStatic", "SceneObject", "NetObject", "SimObject"],
# Projectile Types
"LinearProjectile": ["LinearProjectile", "Projectile", "GameBase", "SceneObject", "NetObject", "SimObject"],
"EnergyProjectile": ["EnergyProjectile", "GrenadeProjectile", "Projectile", "GameBase", "SceneObject", "NetObject", "SimObject"],
"GrenadeProjectile": ["GrenadeProjectile", "Projectile", "GameBase", "SceneObject", "NetObject", "SimObject"],
"TargetProjectile": ["TargetProjectile", "Projectile", "GameBase", "SceneObject", "NetObject", "SimObject"],
# Vehicle Types
"HoverVehicle": ["HoverVehicle", "Vehicle", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
"FlyingVehicle": ["FlyingVehicle", "Vehicle", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
"WheeledVehicle": ["WheeledVehicle", "Vehicle", "ShapeBase", "GameBase", "SceneObject", "NetObject", "SimObject"],
# Datablock Types
"HoverVehicleData": ["HoverVehicleData", "VehicleData", "ShapeBaseData", "GameBaseData", "SimDataBlock", "SimObject"],
"FlyingVehicleData": ["FlyingVehicleData", "VehicleData", "ShapeBaseData", "GameBaseData", "SimDataBlock", "SimObject"],
"WheeledVehicleData": ["WheeledVehicleData", "VehicleData", "ShapeBaseData", "GameBaseData", "SimDataBlock", "SimObject"],
"ForceFieldBareData": ["ForceFieldBareData", "GameBaseData", "SimDataBlock", "SimObject"],
"LinearProjectileData": ["LinearProjectileData", "ProjectileData", "GameBaseData", "SimDataBlock", "SimObject"],
"EnergyProjectileData": ["EnergyProjectileData", "GrenadeProjectileData", "ProjectileData", "GameBaseData", "SimDataBlock", "SimObject"],
"GrenadeProjectileData": ["GrenadeProjectileData", "ProjectileData", "GameBaseData", "SimDataBlock", "SimObject"],
"FireballAtmosphereData": ["FireballAtmosphereData", "GameBaseData", "SimDataBlock", "SimObject"],
"TargetProjectileData": ["TargetProjectileData", "ProjectileData", "GameBaseData", "SimDataBlock", "SimObject"],
}
"""
FIXME: This is a huge pile of filth used for generating the inheritance hierarchy for all the various object types.
"""
# Outputs
global_functions = None
type_methods = None
global_values = None
datablocks = None
def __init__(self, filename):
file_buffer = ""
with open(filename, "r") as handle:
file_buffer = handle.read()
# First, we skip the first 33350 or so because there's lots of declarations
# that the simplified regex will get tripped up on.
chopped_lines = file_buffer.split("\r\n")
chopped_lines = chopped_lines[33350:len(chopped_lines)]
file_buffer = string.join(chopped_lines)
"""
Now we perform a bit of a hack here because of unnecessary immutable
memory bullshit: Strings in Python are immutable and due to the way
the Regex works (can probably be fixed properly at some point),
methods that have a semicolon in their description (most do) will cause
the regex to match up until that semicolon, not the one that actually
delineates the entire method. So as a quick hack, we create a mutable
memory buffer (just a list) to do single character replacements of ;
with ~ within the context of strings. We can't simply use replace or any
of the regular string modification methods because they create copies of
the string memory which bogs down the system massively at this point: times
went down from an absolute unknown to merely ~2sec to run the entirety of this
software using this work around.
"""
mutable_buffer = list(file_buffer)
string_search = re.finditer(self.string_expression, file_buffer)
for string_occurrence in string_search:
string_text = string_occurrence.group(0)
for semi_occurrence in range(string_text.count(";")):
semi_location = string_text.find(";", semi_occurrence)
mutable_buffer[string_occurrence.start() + semi_location] = "~"
# Implode the list together using "" as a delineator, so it just reassembles the payload
file_buffer = string.join(mutable_buffer, "")
# A list of tuples with the following structure: (addr, name, desc, minArgs, maxArgs)
self.global_functions = [ ]
global_method_add_search = re.finditer(self.global_method_add_expression, file_buffer)
for global_function in global_method_add_search:
global_function_source = global_function.group(0)
opening_index = global_function_source.find("(")
closing_index = global_function_source.rfind(")", global_function_source.count(")") - 1)
global_function_source = global_function_source[opening_index + 1:closing_index]
# Extract the description first; this is a huge hack due to the commas in the desc
global_function_source, global_method_description = self._extract_description(global_function_source)
global_method_arguments = global_function_source.split(",")
# Strip out the global method info
global_method_name = self._extract_name(global_method_arguments, 0)
try:
global_method_address = self._extract_address(global_method_arguments, 1)
global_method_minargs = int(global_method_arguments[3])
global_method_maxargs = int(global_method_arguments[4])
self.global_function_count = self.global_function_count + 1
global_function = Function(global_method_name, global_method_address, None, global_method_description, global_method_minargs, global_method_maxargs)
self.global_functions.append(global_function)
except ValueError:
pass
# A dictionary of classname to tuples with the following structure: (typename, addr, name, desc, minArgs, maxArgs)
self.type_methods = { }
self.type_function_counts = { }
type_method_add_search = re.finditer(self.type_method_add_expression, file_buffer)
for type_method in type_method_add_search:
type_method_source = type_method.group(0)
opening_index = type_method_source.find("(")
closing_index = type_method_source.rfind(")")
type_method_source = type_method_source[opening_index + 1:closing_index]
# Extract the description first; this is a huge hack due to the commas in the desc
type_method_source, type_method_description = self._extract_description(type_method_source)
type_method_arguments = type_method_source.split(",")
# Strip out the type method info
type_method_type = self._extract_name(type_method_arguments, 1)
type_method_name = self._extract_name(type_method_arguments, 2)
type_method_address = self._extract_address(type_method_arguments, 3)
try:
type_method_minargs = int(type_method_arguments[5])
type_method_maxargs = int(type_method_arguments[6])
self.type_methods.setdefault(type_method_type, [])
self.type_function_counts.setdefault(type_method_type, 0)
self.type_function_total = self.type_function_total + 1
self.type_function_counts[type_method_type] = self.type_function_counts[type_method_type] + 1
self.type_methods[type_method_type] .append((type_method_type, type_method_address, type_method_name, type_method_description, type_method_minargs, type_method_maxargs))
except ValueError:
continue
self.global_values = [ ]
global_value_add_search = re.finditer(self.global_value_add_expression, file_buffer)
for global_value in global_value_add_search:
global_value_source = global_value.group(0)
opening_index = global_value_source.find("(")
closing_index = global_value_source.rfind(")")
global_value_source = global_value_source[opening_index + 1:closing_index]
global_value_arguments = global_value_source.split(",")
# Strip out the global value info
global_value_name = self._extract_name(global_value_arguments, 0)
global_value_address = self._extract_address(global_value_arguments, 2)
global_value_type = int(global_value_arguments[1])
self.global_values.append(GlobalVariable(global_value_address, global_value_type, 0))
# Extract the datablock properties now
self.datablocks = { }
datablock_property_add_search = re.finditer(self.datablock_property_add_expression, file_buffer)
for datablock_property in datablock_property_add_search:
datablock_property_source = datablock_property.group(0)
"""
Here we don't have to worry about anything with their own scopes
sitting above our declarations in the input file this was built for,
so we just search backwards for the type declaration (Which is always an int)
and use that to copy out the declaration source.
"""
declaration_start = file_buffer.rfind("//----- ", 0, datablock_property.start())
declaration_end = file_buffer.rfind("-", declaration_start, datablock_property.start())
declaration_source = file_buffer[declaration_start:declaration_end]
calling_method = self._extract_caller(declaration_source)
# If we don't know what it is, just add a default value and resolve it this way
self._datablock_type_table.setdefault(calling_method, calling_method)
datablock_type = self._datablock_type_table[calling_method]
self.datablocks.setdefault(datablock_type, Datablock(datablock_type))
# Pull the datablock property information now
datablock_arguments = datablock_property_source.split(",")
datablock_property_name = self._extract_name(datablock_arguments, 0)
datablock_property_address = self._extract_address(datablock_arguments, 2)
# Write it out and we should be fine.
current_datablock = self.datablocks[datablock_type]
current_datablock.properties[datablock_property_name] = Datablock.Property(datablock_property_name, datablock_property_address, "Bla")
# Helper Functions
def _extract_description(self, source):
desc_end = source.rfind("\"")
# We found the end, now we need to look for the previous parameter delineator
desc_begin = -1
ignore_delineator = True # Used for if we're in a quotation
for index in reversed(range(desc_end)):
current_character = source[index]
if (current_character == "," and not ignore_delineator):
desc_begin = index + 1
break
elif (current_character == "\""):
ignore_delineator = not ignore_delineator
desc = source[desc_begin + 1:desc_end]
desc = desc.lstrip()
desc = desc.replace("(int)\"", "")
source = source[0:desc_begin] + source[desc_end:len(source)]
desc = desc.replace("~", ";")
return source, desc
def _extract_name(self, source, index):
name = source[index].lstrip()
name = name[name.find("\"") + 1:len(name)].rstrip("\" ")
# Hack fix for the way the engine registers functions for the Sky type
name = name.replace("(int)&off_7957AC", "Sky")
return name
def _extract_address(self, source, index):
address = source[index]
address = address[address.find("_") + 1:len(address)].rstrip("\" ")
return address.lstrip()
def _extract_caller(self, source):
start = source.find("(")
end = source.find(")", start)
result = int(source[start + 1:end],16)
return hex(result)[2:].upper()

147
t2src.py Executable file
View file

@ -0,0 +1,147 @@
"""
scraper.py
The DokuWiki frontend for generating a web page from the scraper tree
which is in use for the following web page:
http://dx.no-ip.org/doku.php?id=documents:t2engine
This software is licensed under the MIT license. Refer to LICENSE.txt for
details.
Copyright (c) 2016 Robert MacGregor
"""
import re
import time
import string
import scraper
def build_inheritance_tree(inheritance_list, type_names):
inheritance_tree = ""
for tree_element in inheritance_list:
if (tree_element in type_names):
inheritance_tree += "[[#%s]] -> " % tree_element
else:
inheritance_tree += "%s -> " % tree_element
inheritance_tree = inheritance_tree.rstrip(" -> ")
return inheritance_tree
# Main App
class Application(object):
global_method_heading = "===== Global Methods (%u total) =====\r\n"
global_method_arith_heading = "==== Arithmetic Methods (%u total) ====\r\n"
global_method_alx_heading = "==== Audio Methods (%u total) ====\r\n"
global_method_template = "=== %s ===\r\nAddress in Executable: 0x%s\r\n\r\nDescription: %s\r\n\r\nMinimum Arguments: %u\r\n\r\nMaximum Arguments: %u\r\n"
global_value_heading = "===== Global Values (%u total): =====\r\n"
global_value_template = "=== %s ===\r\nType: %s\r\n\r\nAddress in Executable: 0x%s\r\n\r\n"
type_method_heading = "===== Type Methods (%u total methods, %u total types) =====\r\n"
type_name_template = "==== %s ====\r\n%u total native methods\r\n\r\nInheritance: %s\r\n"
type_method_template = "=== %s ===\r\nAddress in Executable: 0x%s\r\n\r\nDescription: %s\r\n\r\nMinimum Arguments: %u\r\n\r\nMaximum Arguments: %u\r\n"
datablock_list_heading = "===== Datablocks (%u total) =====\r\n"
datablock_heading = "==== %s ====\r\nTotal Properties: %u\r\n\r\nInheritance: %s\r\n"
datablock_property_template = "=== %s ===\r\nOffset: %s\r\nType: %s\r\n"
def main(self):
scrape = scraper.Scraper("Tribes2.c")
# Now build a ref file with our compiled information
with open("out.txt", "w") as handle:
handle.write("====== Tribes 2 Engine Reference ======\r\n")
handle.write("Compiled by Robert MacGregor\r\n\r\n")
# Collect specific types of global methods
global_arith_functions = [ ]
global_alx_functions = [ ]
for index, global_function in enumerate(scrape.global_functions):
if (len(global_function.name) != 0 and (global_function.name[0] == "m" or global_function.name.find("Vector") != -1 or global_function.name.find("Matrix") != -1)):
global_arith_functions.append(scrape.global_functions.pop(index))
elif (global_function.name.find("alx") != -1 or global_function.name.find("audio") != -1 or global_function.name.find("getAudio") != -1):
global_alx_functions.append(scrape.global_functions.pop(index))
# Write the Global Method Listing
handle.write(self.global_method_heading % scrape.global_function_count)
handle.write("\r\n")
for global_function in scrape.global_functions:
handle.write(self.global_method_template % (global_function.name, global_function.address, global_function.description, global_function.min_args - 1, global_function.max_args - 1))
handle.write("\r\n")
# Arithmetic Global Methods
handle.write(self.global_method_arith_heading % len(global_arith_functions))
handle.write("\r\n")
for global_arith_function in global_arith_functions:
handle.write(self.global_method_template % (global_arith_function.name, global_arith_function.address, global_arith_function.address, global_arith_function.min_args - 1, global_arith_function.max_args - 1))
handle.write("\r\n")
# Audio Global Methods
handle.write(self.global_method_alx_heading % len(global_alx_functions))
handle.write("\r\n")
for global_alx_function in global_alx_functions:
handle.write(self.global_method_template % (global_alx_function.name, global_alx_function.address, global_alx_function.description, global_alx_function.min_args - 1, global_alx_function.max_args - 1))
handle.write("\r\n")
# Write out the known types
#handle.write("
# Now the type methods
handle.write(self.type_method_heading % (scrape.type_function_total, len(scrape.type_methods.keys())))
handle.write("\r\n")
for type_name in scrape.type_methods.keys():
inheritance_tree = "<Unknown>"
if (type_name in scrape.type_name_inheritance.keys()):
inheritance_tree = build_inheritance_tree(scrape.type_name_inheritance[type_name], scrape.type_methods.keys())
handle.write(self.type_name_template % (type_name, len(scrape.type_methods[type_name]), inheritance_tree))
# Native Methods First
for type_method_type, type_method_address, type_method_name, type_method_description, type_method_minargs, type_method_maxargs in scrape.type_methods[type_name]:
handle.write(self.type_method_template % (type_method_name, type_method_address, type_method_description, type_method_minargs - 1, type_method_maxargs - 1))
handle.write("\r\n")
# And Global Values
handle.write(self.global_value_heading % len(scrape.global_values))
handle.write("\r\n")
for global_value in scrape.global_values:
if (global_value.name[0] != "$"):
global_value.name = "%s%s" % ("$", global_value.name)
handle.write(self.global_value_template % (global_value.name, scrape.primitive_type_mapping[global_value.type_name], global_value.address))
handle.write("\r\n")
# Write datablocks
handle.write(self.datablock_list_heading % len(scrape.datablocks.keys()))
for datablock_type in scrape.datablocks.keys():
inheritance_tree = "<Unknown>"
if (datablock_type in scrape.type_name_inheritance.keys()):
inheritance_tree = build_inheritance_tree(scrape.type_name_inheritance[datablock_type], scrape.type_name_inheritance[datablock_type])
datablock = scrape.datablocks[datablock_type]
handle.write(self.datablock_heading % (datablock.name, len(datablock.properties.keys()), inheritance_tree))
for datablock_property_name in datablock.properties.keys():
datablock_property = datablock.properties[datablock_property_name]
handle.write(self.datablock_property_template % (datablock_property.name, datablock_property.address, datablock_property.type_name))
if __name__ == "__main__":
time_before = time.time()
Application().main()
time_after = time.time()
print("Processed in %f seconds" % (time_after - time_before))