Apertura del repositorio

This commit is contained in:
2025-05-24 18:09:39 -03:00
parent 76e6359dad
commit d883ddd0d0
35253 changed files with 2891973 additions and 2 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python
"""\
This script replaces tab characters with spaces in source code files.
$LicenseInfo:firstyear=2024&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2024, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
import os
def convert_tabs_to_spaces(file_path, tab_stop):
"""Convert tabs in a file to spaces, considering tab stops."""
with open(file_path, 'r') as file:
lines = file.readlines()
# Skip files with no tabs
if not any('\t' in line for line in lines):
return
new_lines = []
for line in lines:
# Remove trailing spaces
line = line.rstrip()
new_line = ''
column = 0 # Track the column index for calculating tab stops
for char in line:
if char == '\t':
# Calculate spaces needed to reach the next tab stop
spaces_needed = tab_stop - (column % tab_stop)
new_line += ' ' * spaces_needed
column += spaces_needed
else:
new_line += char
column += 1
new_lines.append(new_line + '\n')
with open(file_path, 'w', newline='\n') as file:
file.writelines(new_lines)
def process_directory(directory, extensions, tab_stop):
"""Recursively process files in directory, considering tab stops."""
extensions = tuple(extensions)
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(extensions):
file_path = os.path.join(root, file)
print(f"Processing {file_path}")
convert_tabs_to_spaces(file_path, tab_stop)
def main():
parser = argparse.ArgumentParser(description='Convert tabs to spaces in files, considering tab stops.')
parser.add_argument('-e', '--extensions', type=str, default='c,cpp,h,hpp,inl,py,glsl,cmake', help='Comma-separated list of file extensions to process (default: "c,cpp,h,hpp,inl,py,glsl,cmake")')
parser.add_argument('-t', '--tabstop', type=int, default=4, help='Tab stop size (default: 4)')
parser.add_argument('-d', '--directory', type=str, required=True, help='Directory to process')
args = parser.parse_args()
extensions = args.extensions.split(',')
# Add a dot prefix to each extension if not present
extensions = [ext if ext.startswith('.') else f".{ext}" for ext in extensions]
process_directory(args.directory, extensions, args.tabstop)
print("Processing completed.")
if __name__ == "__main__":
main()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""\
This script formats XML files in a given directory with options for indentation and space removal.
$LicenseInfo:firstyear=2023&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2023, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import os
import sys
import glob
import io
import xml.etree.ElementTree as ET
def get_xml_declaration(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
first_line = file.readline().strip()
if first_line.startswith('<?xml'):
return first_line
return None
def parse_xml_file(file_path):
try:
tree = ET.parse(file_path)
return tree
except ET.ParseError as e:
print(f"Error parsing XML file {file_path}: {e}")
return None
def indent(elem, level=0, indent_text=False, indent_tab=False):
indent_string = "\t" if indent_tab else " "
i = "\n" + level * indent_string
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + indent_string
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent(elem, level + 1, indent_text, indent_tab)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
if indent_text and elem.text and not elem.text.isspace():
elem.text = "\n" + (level + 1) * indent_string + elem.text.strip() + "\n" + level * indent_string
def save_xml(tree, file_path, xml_decl, indent_text=False, indent_tab=False, rm_space=False, rewrite_decl=False):
if tree is not None:
root = tree.getroot()
indent(root, indent_text=indent_text, indent_tab=indent_tab)
xml_string = ET.tostring(root, encoding='unicode')
if rm_space:
xml_string = xml_string.replace(' />', '/>')
xml_decl = (xml_decl if (xml_decl and not rewrite_decl)
else '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>')
try:
with io.open(file_path, 'wb') as file:
file.write(xml_decl.encode('utf-8'))
file.write('\n'.encode('utf-8'))
if xml_string:
file.write(xml_string.encode('utf-8'))
if not xml_string.endswith('\n'):
file.write('\n'.encode('utf-8'))
except IOError as e:
print(f"Error saving file {file_path}: {e}")
def process_directory(directory_path, indent_text=False, indent_tab=False, rm_space=False, rewrite_decl=False):
if not os.path.isdir(directory_path):
print(f"Directory not found: {directory_path}")
return
xml_files = glob.glob(os.path.join(directory_path, "*.xml"))
if not xml_files:
print(f"No XML files found in directory: {directory_path}")
return
for file_path in xml_files:
xml_decl = get_xml_declaration(file_path)
tree = parse_xml_file(file_path)
if tree is not None:
save_xml(tree, file_path, xml_decl, indent_text, indent_tab, rm_space, rewrite_decl)
if __name__ == "__main__":
if len(sys.argv) < 2 or '--help' in sys.argv:
print("This script formats XML files in a given directory. Useful to fix XUI XMLs after processing by other tools.")
print("\nUsage:")
print(" python fix_xml_indentations.py <path/to/directory> [options]")
print("\nOptions:")
print(" --indent-text Indents text within XML tags.")
print(" --indent-tab Uses tabs instead of spaces for indentation.")
print(" --rm-space Removes spaces in self-closing tags.")
print(" --rewrite_decl Replaces the XML declaration line.")
print("\nCommon Usage:")
print(" To format XML files with text indentation, tab indentation, and removal of spaces in self-closing tags:")
print(" python fix_xml_indentations.py /path/to/xmls --indent-text --indent-tab --rm-space")
sys.exit(1)
directory_path = sys.argv[1]
indent_text = '--indent-text' in sys.argv
indent_tab = '--indent-tab' in sys.argv
rm_space = '--rm-space' in sys.argv
rewrite_decl = '--rewrite_decl' in sys.argv
process_directory(directory_path, indent_text, indent_tab, rm_space, rewrite_decl)
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# $LicenseInfo:firstyear=2014&license=viewerlgpl$
# Second Life Viewer Source Code
# Copyright (C) 2011, Linden Research, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License only.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
#
###
### Extract strings modified between some version and the current version
###
Action=DEFAULT
Rev=master
DefaultXuiDir="indra/newview/skins/default/xui"
Verbose=false
ExitStatus=0
while [ $# -ne 0 ]
do
case ${1} in
##
## Show usage
##
-h|--help)
Action=USAGE
;;
-v|--verbose)
Verbose=true
;;
##
## Select the revision to compare against
##
-r)
if [ $# -lt 2 ]
then
echo "Must specify <revision> with ${1}" 1>&2
Action=USAGE
ExitStatus=1
break
else
Rev=${2}
shift # consume the switch ( for n values, consume n-1 )
fi
;;
##
## handle an unknown switch
##
-*)
Action=USAGE
ExitStatus=1
break
;;
*)
if [ -z "${XuiDir}" ]
then
XuiDir=${1}
else
echo "Too many arguments supplied: $@" 1>&2
Action=USAGE
ExitStatus=1
break
fi
;;
esac
shift # always consume 1
done
progress()
{
if $Verbose
then
echo $* 1>&2
fi
}
if [[ $ExitStatus -eq 0 && "${Action}" = "DEFAULT" ]]
then
if [[ ! -d "${XuiDir:=$DefaultXuiDir}" ]]
then
echo "No XUI directory found in '$XuiDir'" 1>&2
Action=USAGE
ExitStatus=1
fi
fi
if [ "${Action}" = "USAGE" ]
then
cat <<USAGE
Usage:
modified-strings.sh [ { -v | --verbose } ] [-r <revision>] [<path-to-xui>]
where
--verbose shows progress messages on stderr (the command takes a while, so this is reassuring)
-r <revision> specifies a git revision (branch, tag, commit, or relative specifier)
defaults to 'master' so that comparison is against the HEAD of the released viewer branch
<path-to-xui> is the path to the root directory for XUI files
defaults to '$DefaultXuiDir'
Emits a tab-separated file with these columns:
filename
the path of a file that has a string change (columns 2 and 3 are empty for lines with a filename)
name
the name attribute of a string or label whose value changed
English value
the current value of the string or label whose value changed
for strings, newlines are changed to '\n' and tab characters are changed to '\t'
There is also a column for each of the language directories following the English.
USAGE
exit $ExitStatus
fi
stringval() # reads stdin and prints the escaped value of a string for the requested tag
{
local tag=$1
xmllint --xpath "string(/strings/string[@name=\"$tag\"])" - | perl -p -e 'chomp; s/\n/\\n/g; s/\t/\\t/g;'
}
columns="file\tname\tEN"
for lang in $(ls -1 ${XuiDir})
do
if [[ "$lang" != "en" && -d "${XuiDir}" && -f "${XuiDir}/$lang/strings.xml" ]]
then
columns+="\t$lang"
fi
done
echo -e "$columns"
EnglishStrings="${XuiDir}/en/strings.xml"
progress -n "scanning $EnglishStrings "
echo -e "$EnglishStrings"
# loop over all tags in the current version of the strings file
cat "$EnglishStrings" | xmllint --xpath '/strings/string/@name' - | sed 's/ name="//; s/"$//;' \
| while read name
do
progress -n "."
# fetch the $Rev and current values for each tag
old_stringval=$(git show "$Rev:$EnglishStrings" 2> /dev/null | stringval "$name")
new_stringval=$(cat "$EnglishStrings" | stringval "$name")
if [[ "$old_stringval" != "$new_stringval" ]]
then
# the value is different, so print the tag and it's current value separated by a tab
echo -e "\t$name\t$new_stringval"
fi
done
progress ""
# loop over all XUI files other than strings.xml finding labels
grep -rlw 'label' "${XuiDir}/en" | grep -v '/strings.xml' \
| while read xuipath
do
progress -n "scanning $xuipath "
listed_file=false
# loop over all elements for which there is a label attribute, getting the name attribute value
xmllint --xpath '//*[@label]/@name' "$xuipath" 2> /dev/null | sed 's/ name="//; s/"$//;' \
| while read name
do
progress -n "."
# get the old and new label attribute values for each name
old_label=$(git show "$Rev:$xuipath" 2> /dev/null | xmllint --xpath "string(//*[@name=\"${name}\"]/@label)" - 2> /dev/null)
new_label=$(cat "$xuipath" | xmllint --xpath "string(//*[@name=\"${name}\"]/@label)" - 2> /dev/null)
if [[ "$old_label" != "$new_label" ]]
then
if ! $listed_file
then
echo -e "$xuipath"
listed_file=true
fi
echo -e "\t$name\t$new_label"
fi
done
progress ""
done
+407
View File
@@ -0,0 +1,407 @@
#!/usr/bin/env python3
"""\
This script scans the SL codebase for translation-related strings.
$LicenseInfo:firstyear=2020&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2020, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import xml.etree.ElementTree as ET
import argparse
import os
import sys
from git import Repo, Git # requires the gitpython package
import pandas as pd
import re
from datetime import datetime
usage_msg="""%(prog)s [options]
Analyze the XUI configuration files to find text that may need to
be translated. Works by comparing two specified revisions, one
specified by --rev (default HEAD) and one specified by --rev_base
(default master). The script works by comparing xui contents of the
two revisions, and outputs a spreadsheet listing any areas of
difference. The target language must be specified using the --lang
option. Output is an excel file, which can be used as-is or imported
into google sheets.
If the --rev revision already contains a translation for the text, it
will be included in the spreadsheet for reference.
Normally you would want --rev_base to be the last revision to have
translations added, and --rev to be the tip of the current
project. You can find the last commit with translation work using "git log --grep INTL- | head"
The --missing argument can be used to find all text with missing
translations, regardless of when it was added. If translations are being kept
reasonably current, you will normally not need this argument.
"""
translate_attribs = [
"title",
"short_title",
"value",
"label",
"label_selected",
"tool_tip",
"ignoretext",
"yestext",
"notext",
"canceltext",
"description",
"longdescription"
]
def codify_for_print(val):
if isinstance(val, str):
return val.encode("utf-8")
else:
return str(val, 'utf-8').encode("utf-8")
# Returns a dict of { name => xml_node }
def read_xml_elements(blob):
try:
contents = blob.data_stream.read()
except:
# default - pretend we read a file with no elements of interest.
# Parser will complain if it gets no elements at all.
contents = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?><strings></strings>'
xml = ET.fromstring(contents)
elts = {}
for child in xml.iter():
if "name" in child.attrib:
name = child.attrib['name']
elts[name] = child
return elts
def failure(*msg):
print(*msg)
sys.exit(1)
# return True iff any element of lis is "in" thing
def has_any(thing,lis):
for l in lis:
if l in thing:
return True
return False
def should_translate(filename, elt, field, val):
if val is None:
return False
# Should translate apply recursively?
if "translate" in elt.attrib and elt.attrib["translate"] == "false":
return False
if has_any(filename,["floater_test","floater_aaa","floater_ui_preview"]):
return False
if "TestString PleaseIgnore" in val:
return False
val = re.sub(r"\[.*?\]","",val)
if len(val) == 0:
return False
if val.isspace():
return False
val = val.strip()
if val.isdigit():
return False
if not re.search('\w+', val):
return False
if re.match(r"^\s*\d*\s*x\s*\d*\s*$", val):
#print(val, "matches resolution string, will ignore")
return False
# "value" attribute is a hairball, mostly used to encode non-display info but a few exceptions
if field == "value":
if elt.text is not None and len(elt.text) > 0:
#print("value has text, ignoring", ET.tostring(elt))
return False
if has_any(elt.attrib,["label"]):
return False
if elt.tag in ["string","text"]:
return True
#print("including value attribute", val, "tag", elt.tag,"in", ET.tostring(elt))
return True
return True
def make_translation_table(mod_tree, base_tree, lang, args):
xui_path = "{}/{}".format(xui_base, args.base_lang)
try:
mod_xui_tree = mod_tree[xui_path]
except:
failure("xui tree not found for base language", args.base_lang,"or target lang", lang)
if args.rev == args.rev_base:
failure("Revs are the same, nothing to compare")
data = []
# For all files to be checked for translations
all_en_strings = set()
for mod_blob in mod_xui_tree.traverse():
filename = mod_blob.path
if mod_blob.type == "tree": # directory, skip
continue
if args.files and os.path.basename(filename) not in args.files:
continue # process only the specified files
if args.verbose:
print(filename)
try:
base_blob = base_tree[filename]
except:
if args.verbose:
print("No matching base file found for", filename)
base_blob = None
try:
transl_filename = filename.replace("/xui/{}/".format(args.base_lang), "/xui/{}/".format(lang))
transl_blob = mod_tree[transl_filename]
except:
if args.verbose:
print("No matching translation file found at", transl_filename)
transl_blob = None
mod_dict = read_xml_elements(mod_blob)
base_dict = read_xml_elements(base_blob)
transl_dict = read_xml_elements(transl_blob)
rows = 0
for name in list(mod_dict.keys()):
if not name in base_dict or mod_dict[name].text != base_dict[name].text or (args.missing and not name in transl_dict):
elt = mod_dict[name]
val = elt.text
field = "text"
if should_translate(filename, elt, field, val):
transl_val = "--"
if name in transl_dict:
transl_val = transl_dict[name].text
if val in all_en_strings:
new_val = "(DUPLICATE)"
else:
new_val = ""
data.append([val, transl_val, new_val, "", "", filename, name, field])
all_en_strings.add(val)
rows += 1
for attr in translate_attribs:
if attr in mod_dict[name].attrib:
if name not in base_dict \
or attr not in base_dict[name].attrib \
or mod_dict[name].attrib[attr] != base_dict[name].attrib[attr] \
or (args.missing and (not name in transl_dict or not attr in transl_dict[name].attrib)):
elt = mod_dict[name]
val = elt.attrib[attr]
if should_translate(filename, elt, attr, val):
transl_val = "--"
if name in transl_dict and attr in transl_dict[name].attrib:
transl_val = transl_dict[name].attrib[attr]
if val in all_en_strings:
new_val = "(DUPLICATE)"
else:
new_val = ""
#attr = attr + ":" + ET.tostring(elt)
data.append([val, transl_val, new_val, "", "", filename, name, attr])
all_en_strings.add(val)
rows += 1
return data
def find_deletions(mod_tree, base_tree, lang, args, f):
transl_xui_path = "{}/{}".format(xui_base, lang)
try:
transl_xui_tree = mod_tree[transl_xui_path]
except:
failure("xui tree not found for base language", args.base_lang,"or target lang", lang)
for transl_blob in transl_xui_tree.traverse():
if transl_blob.type == "tree": # directory, skip
continue
transl_filename = transl_blob.path
mod_filename = transl_filename.replace("/xui/{}/".format(lang), "/xui/{}/".format(args.base_lang))
#print("checking",transl_filename,"against",mod_filename)
try:
mod_blob = mod_tree[mod_filename]
except:
print(" delete file", transl_filename, file=f)
continue
mod_dict = read_xml_elements(mod_blob)
if len(mod_dict) == 0:
print(" delete file", transl_filename, file=f)
continue
transl_dict = read_xml_elements(transl_blob)
#print("mod vs transl", len(mod_dict), len(transl_dict))
lines = 0
for elt_key in transl_dict:
if not elt_key in mod_dict:
if lines == 0:
print(" in file", transl_filename, file=f)
lines += 1
print(" delete element", elt_key, file=f)
else:
transl_elt = transl_dict[elt_key]
mod_elt = mod_dict[elt_key]
for a in transl_elt.attrib:
if not a in mod_elt.attrib:
if lines == 0:
print(" in file", transl_filename, file=f)
lines += 1
print(" delete attribute", a, "from", elt_key, file=f)
if transl_elt.text and (not mod_elt.text):
if lines == 0:
print(" in file", transl_filename, file=f)
lines += 1
print(" delete text from", elt_key, file=f)
def save_translation_file(per_lang_data, aux_data, outfile):
langs = sorted(per_lang_data.keys())
print("Saving languages", ",".join(langs),"as",outfile)
writer = pd.ExcelWriter(outfile, engine='xlsxwriter')
workbook = writer.book
wrap_format = workbook.add_format({'text_wrap': True})
bold_wrap_format = workbook.add_format({'text_wrap': True, 'bold': True})
wrap_unlocked_format = workbook.add_format({'text_wrap': True, 'locked': False})
for lang in langs:
data = per_lang_data[lang]
num_translations = len(data)
cols = ["EN", "Previous Translation ({})".format(lang.upper()), "ENTER NEW TRANSLATION ({})".format(lang.upper()), "Translator Questions", "Notes", "File", "Element", "Field"]
df = pd.DataFrame(data, columns=cols)
df.to_excel(writer, index=False, sheet_name = lang.upper())
worksheet = writer.sheets[lang.upper()]
# Translators primarily care about columns A-C, and should write
# only in column C. Hide the others. Set widths.
worksheet.protect()
worksheet.set_column('A:B', 60, wrap_format)
worksheet.set_column('C:C', 60, wrap_unlocked_format)
worksheet.set_column('D:E', 40, wrap_unlocked_format)
worksheet.set_column('F:F', 50, wrap_format, {'hidden': True})
worksheet.set_column('G:H', 30, wrap_format, {'hidden': True})
# Lock the top row (column headers) in place while scrolling
worksheet.freeze_panes(1, 0)
print("Added", num_translations, "rows for language", lang)
# Reference info, not for translation
for aux, data in list(aux_data.items()):
df = pd.DataFrame(data, columns = ["Key", "Value"])
df.to_excel(writer, index=False, sheet_name=aux)
worksheet = writer.sheets[aux]
worksheet.set_column('A:A', 50, bold_wrap_format)
worksheet.set_column('B:B', 80, wrap_format)
print("Writing", outfile)
writer.save()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="analyze viewer xui files for needed translations", usage=usage_msg)
parser.add_argument("-v","--verbose", action="store_true", help="verbose flag")
parser.add_argument("--missing", action="store_true", default = False, help="include all fields for which a translation does not exist")
parser.add_argument("--deleted", action="store_true", default = False, help="show all translated entities which don't exist in english")
parser.add_argument("--skip_spreadsheet", action="store_true", default = False, help="skip creating the translation spreadsheet")
parser.add_argument("--rev", help="revision with modified strings, default HEAD", default="HEAD")
parser.add_argument("--rev_base", help="previous revision to compare against, default main", default="main")
parser.add_argument("--base_lang", help="base language, default en (normally leave unchanged - other values are only useful for testing)", default="en")
parser.add_argument("--lang", help="target languages, or 'all_valid' or 'supported'; default is 'supported'", nargs="+", default = ["supported"])
parser.add_argument("--files", help='list of files to process', metavar='F', type=str, nargs='*')
parser.add_argument("--outfile", help='name of the output file', type=str, nargs='?', default="SL_Translations.xlsx")
args = parser.parse_args()
cwd = os.getcwd()
rootdir = Git(cwd).rev_parse("--show-toplevel")
repo = Repo(rootdir)
try:
mod_commit = repo.commit(args.rev)
except:
failure(args.rev,"is not a valid commit")
try:
base_commit = repo.commit(args.rev_base)
except:
failure(args.rev_base,"is not a valid commit")
print("Will identify changes in", args.rev, "not present in", args.rev_base)
if args.missing:
print("Will also include any text for which no corresponding translation exists, regardless of when it was added")
sys.stdout.flush()
mod_tree = mod_commit.tree
base_tree = base_commit.tree
xui_base = "indra/newview/skins/default/xui"
xui_base_tree = mod_tree[xui_base]
# Find target languages
# all languages present in the codebase
valid_langs = [tree.name.lower() for tree in xui_base_tree if tree.name.lower() != args.base_lang.lower()]
# offically supported languages
supported_langs = ["fr", "es", "it", "pt", "ja", "de"]
langs = [l.lower() for l in args.lang]
if "supported" in args.lang:
langs = supported_langs
if "all_valid" in args.lang:
langs = valid_langs
langs = sorted(langs)
for lang in langs:
if not lang in valid_langs:
failure("Unknown target language {}. Valid values are {}".format(lang,", ".join(sorted(valid_langs) + ["all_valid","supported"])))
print("Target language(s) are", ",".join(sorted(langs)))
sys.stdout.flush()
outfile = args.outfile
try:
f = open(outfile,"a+")
f.close()
except:
failure("Can't write to output file",outfile,". Is it already open?")
aux_data = { "REFERENCE": [["Command", " ".join(sys.argv)],
["Date", str(datetime.now())],
["Mod Commit", mod_commit.hexsha],
["Base Commit", base_commit.hexsha],
] }
if not args.skip_spreadsheet:
per_lang_data = {}
for lang in langs:
print("Creating spreadsheet for language", lang)
sys.stdout.flush()
per_lang_data[lang] = make_translation_table(mod_tree, base_tree, lang, args)
print("Saving output file", outfile)
save_translation_file(per_lang_data, aux_data, outfile)
if args.deleted:
deletion_file = "Translate_deletions.txt"
print("Saving deletion info to", deletion_file)
with open(deletion_file,"w") as f:
for lang in langs:
find_deletions(mod_tree, base_tree, lang, args, f)
+627
View File
@@ -0,0 +1,627 @@
#!/bin/bash
###
### Constants
###
TRUE=0 # Map the shell's idea of truth to a variable for better documentation
FALSE=1
#echo "DEBUG ARGS: $@"
#echo "DEBUG `pwd`"
# args ../indra
# <string>-DCMAKE_BUILD_TYPE:STRING=Release</string>
# <string>-DADDRESS_SIZE:STRING=32</string>
# <string>-DROOT_PROJECT_NAME:STRING=SecondLife</string>
# <string>-DFMODSTUDIO:BOOL=ON</string>
# <string>-DOPENSIM:BOOL=ON</string>
# <string>-DUSE_AVX_OPTIMIZATION:BOOL=OFF</string>
# <string>-DUSE_AVX2_OPTIMIZATION:BOOL=OFF</string>
# <string>-DLL_TESTS:BOOL=OFF</string>
# <string>-DPACKAGE:BOOL=OFF></string>
###
### Global Variables
###
WANTS_CLEAN=$FALSE
WANTS_CONFIG=$FALSE
WANTS_PACKAGE=$FALSE
WANTS_VERSION=$FALSE
WANTS_FMODSTUDIO=$FALSE
WANTS_OPENAL=$FALSE
WANTS_OPENSIM=$TRUE
WANTS_SINGLEGRID=$FALSE
WANTS_HAVOK=$FALSE
WANTS_AVX=$FALSE
WANTS_AVX2=$FALSE
WANTS_TESTBUILD=$FALSE
WANTS_TRACY=$FALSE
WANTS_BUILD=$FALSE
WANTS_CRASHREPORTING=$FALSE
WANTS_CACHE=$FALSE
TARGET_PLATFORM="darwin" # darwin, windows, linux
BTYPE="Release"
CHANNEL="" # will be overwritten later with platform-specific values unless manually specified.
LL_ARGS_PASSTHRU=""
JOBS="0"
WANTS_NINJA=$FALSE
WANTS_VSCODE=$FALSE
USE_VSTOOL=$FALSE
TESTBUILD_PERIOD="0"
SINGLEGRID_URI=""
###
### Helper Functions
###
showUsage()
{
echo
echo "Usage: "
echo "========================"
echo
echo " --clean : Remove past builds & configuration"
echo " --config : Generate a new architecture-specific config"
echo " --build : Build Puma"
echo " --version : Update version number"
echo " --chan [Release|Beta|Private] : Private is the default, sets channel"
echo " --btype [Release|RelWithDebInfo] : Release is default, whether to use symbols"
echo " --package : Build installer"
echo " --no-package : Build without installer (Overrides --package)"
echo " --fmodstudio : Build with FMOD Studio"
echo " --openal : Build with OpenAL"
echo " --opensim : Build with OpenSim support (Disables Havok features)"
echo " --no-opensim : Build without OpenSim support (Overrides --opensim)"
echo " --singlegrid <login_uri> : Build for single grid usage (Requires --opensim)"
echo " --havok : Build with Havok support (Disables OpenSim support)"
echo " --avx : Build with Advanced Vector Extensions"
echo " --avx2 : Build with Advanced Vector Extensions 2"
echo " --tracy : Build with Tracy Profiler support"
echo " --crashreporting : Build with crash reporting enabled (Windows only)"
echo " --testbuild <days> : Create time-limited test build (build date + <days>)"
echo " --platform <platform> : Build for specified platform (darwin | windows | linux)"
echo " --jobs <num> : Build with <num> jobs in parallel (Linux and Darwin only)"
echo " --ninja : Build using Ninja"
echo " --vscode : Exports compile commands for VSCode (Linux only)"
echo " --compiler-cache : Try to detect and use compiler cache (needs also --ninja for OSX and Windows)"
echo " --vstools : Use vstool to setup project startup properties (Windows only)"
echo
echo "All arguments not in the above list will be passed through to LL's configure/build."
echo
}
getArgs()
# $* = the options passed in from main
{
if [ $# -gt 0 ]; then
while getoptex "clean build config version package no-package fmodstudio openal ninja vscode compiler-cache vstools jobs: platform: opensim no-opensim singlegrid: havok avx avx2 tracy crashreporting testbuild: help chan: btype:" "$@" ; do
#ensure options are valid
if [ -z "$OPTOPT" ] ; then
showUsage
exit 1
fi
case "$OPTOPT" in
clean) WANTS_CLEAN=$TRUE;;
config) WANTS_CONFIG=$TRUE;;
version) WANTS_VERSION=$TRUE;;
chan) CHANNEL="$OPTARG";;
btype) if [ \( "$OPTARG" == "Release" \) -o \( "$OPTARG" == "RelWithDebInfo" \) ] ; then
BTYPE="$OPTARG"
fi
;;
fmodstudio) WANTS_FMODSTUDIO=$TRUE;;
openal) WANTS_OPENAL=$TRUE;;
opensim) WANTS_OPENSIM=$TRUE;;
no-opensim) WANTS_OPENSIM=$FALSE;;
singlegrid) WANTS_SINGLEGRID=$TRUE
SINGLEGRID_URI="$OPTARG"
;;
havok) WANTS_HAVOK=$TRUE
WANTS_OPENSIM=$FALSE
;;
avx) WANTS_AVX=$TRUE;;
avx2) WANTS_AVX2=$TRUE;;
tracy) WANTS_TRACY=$TRUE;;
crashreporting) WANTS_CRASHREPORTING=$TRUE;;
testbuild) WANTS_TESTBUILD=$TRUE
TESTBUILD_PERIOD="$OPTARG"
;;
package) WANTS_PACKAGE=$TRUE;;
no-package) WANTS_PACKAGE=$FALSE;;
build) WANTS_BUILD=$TRUE;;
platform) TARGET_PLATFORM="$OPTARG";;
jobs) JOBS="$OPTARG";;
ninja) WANTS_NINJA=$TRUE;;
vscode) WANTS_VSCODE=$TRUE;;
compiler-cache) WANTS_CACHE=$TRUE;;
vstools) USE_VSTOOL=$TRUE;;
help) showUsage && exit 0;;
-*) showUsage && exit 1;;
*) showUsage && exit 1;;
esac
done
shift $[OPTIND-1]
if [ $OPTIND -le 1 ] ; then
showUsage && exit 1
fi
fi
if [ $WANTS_CLEAN -ne $TRUE ] && [ $WANTS_CONFIG -ne $TRUE ] && \
[ $WANTS_VERSION -ne $TRUE ] && [ $WANTS_BUILD -ne $TRUE ] && \
[ $WANTS_PACKAGE -ne $TRUE ] ; then
# the user didn't say what to do, so assume he wants to do a basic rebuild
WANTS_CONFIG=$TRUE
WANTS_BUILD=$TRUE
WANTS_VERSION=$TRUE
fi
LOG="`pwd`/logs/build_$TARGET_PLATFORM.log"
if [ -r "$LOG" ] ; then
rm -f `basename "$LOG"`/* #(remove old logfiles)
fi
}
function b2a()
{
if [ $1 -eq $TRUE ] ; then
echo "true"
else
echo "false"
fi
}
function getoptex()
{
let $# || return 1
local optlist="${1#;}"
let OPTIND || OPTIND=1
[ $OPTIND -lt $# ] || return 1
shift $OPTIND
if [ "$1" != "-" -a "$1" != "${1#-}" ]
then OPTIND=$[OPTIND+1]; if [ "$1" != "--" ]
then
local o
o="-${1#-$OPTOFS}"
for opt in ${optlist#;}
do
OPTOPT="${opt%[;.:]}"
unset OPTARG
local opttype="${opt##*[^;:.]}"
[ -z "$opttype" ] && opttype=";"
if [ ${#OPTOPT} -gt 1 ]
then # long-named option
case $o in
"--$OPTOPT")
if [ "$opttype" != ":" ]; then return 0; fi
OPTARG="$2"
if [ -z "$OPTARG" ];
then # error: must have an agrument
let OPTERR && echo "$0: error: $OPTOPT must have an argument" >&2
OPTARG="$OPTOPT";
OPTOPT="?"
return 1;
fi
OPTIND=$[OPTIND+1] # skip option's argument
return 0
;;
"--$OPTOPT="*)
if [ "$opttype" = ";" ];
then # error: must not have arguments
let OPTERR && echo "$0: error: $OPTOPT must not have arguments" >&2
OPTARG="$OPTOPT"
OPTOPT="?"
return 1
fi
OPTARG=${o#"--$OPTOPT="}
return 0
;;
esac
else # short-named option
case "$o" in
"-$OPTOPT")
unset OPTOFS
[ "$opttype" != ":" ] && return 0
OPTARG="$2"
if [ -z "$OPTARG" ]
then
echo "$0: error: -$OPTOPT must have an argument" >&2
OPTARG="$OPTOPT"
OPTOPT="?"
return 1
fi
OPTIND=$[OPTIND+1] # skip option's argument
return 0
;;
"-$OPTOPT"*)
if [ $opttype = ";" ]
then # an option with no argument is in a chain of options
OPTOFS="$OPTOFS?" # move to the next option in the chain
OPTIND=$[OPTIND-1] # the chain still has other options
return 0
else
unset OPTOFS
OPTARG="${o#-$OPTOPT}"
return 0
fi
;;
esac
fi
done
#echo "$0: error: invalid option: $o"
LL_ARGS_PASSTHRU="$LL_ARGS_PASSTHRU $o"
return 0
#showUsage
#exit 1
fi; fi
OPTOPT="?"
unset OPTARG
return 1
}
function optlistex
{
local l="$1"
local m # mask
local r # to store result
while [ ${#m} -lt $[${#l}-1] ]; do m="$m?"; done # create a "???..." mask
while [ -n "$l" ]
do
r="${r:+"$r "}${l%$m}" # append the first character of $l to $r
l="${l#?}" # cut the first charecter from $l
m="${m#?}" # cut one "?" sign from m
if [ -n "${l%%[^:.;]*}" ]
then # a special character (";", ".", or ":") was found
r="$r${l%$m}" # append it to $r
l="${l#?}" # cut the special character from l
m="${m#?}" # cut one more "?" sign
fi
done
echo $r
}
function getopt()
{
local optlist=`optlistex "$1"`
shift
getoptex "$optlist" "$@"
return $?
}
###
### Main Logic
###
getArgs $*
if [ ! -d `dirname "$LOG"` ] ; then
mkdir -p `dirname "$LOG"`
fi
echo -e "configure_puma.sh" > "$LOG"
echo -e " PLATFORM: $TARGET_PLATFORM" | tee -a "$LOG"
echo -e " FMODSTUDIO: `b2a $WANTS_FMODSTUDIO`" | tee -a "$LOG"
echo -e " OPENAL: `b2a $WANTS_OPENAL`" | tee -a "$LOG"
echo -e " OPENSIM: `b2a $WANTS_OPENSIM`" | tee -a "$LOG"
if [ $WANTS_SINGLEGRID -eq $TRUE ] ; then
echo -e " SINGLEGRID: `b2a $WANTS_SINGLEGRID` ($SINGLEGRID_URI)" | tee -a "$LOG"
else
echo -e " SINGLEGRID: `b2a $WANTS_SINGLEGRID`" | tee -a "$LOG"
fi
echo -e " HAVOK: `b2a $WANTS_HAVOK`" | tee -a "$LOG"
echo -e " AVX: `b2a $WANTS_AVX`" | tee -a "$LOG"
echo -e " AVX2: `b2a $WANTS_AVX2`" | tee -a "$LOG"
echo -e " TRACY: `b2a $WANTS_TRACY`" | tee -a "$LOG"
echo -e " CRASHREPORTING: `b2a $WANTS_CRASHREPORTING`" | tee -a "$LOG"
if [ $WANTS_TESTBUILD -eq $TRUE ] ; then
echo -e " TESTBUILD: `b2a $WANTS_TESTBUILD` ($TESTBUILD_PERIOD days)" | tee -a "$LOG"
else
echo -e " TESTBUILD: `b2a $WANTS_TESTBUILD`" | tee -a "$LOG"
fi
echo -e " PACKAGE: `b2a $WANTS_PACKAGE`" | tee -a "$LOG"
echo -e " CLEAN: `b2a $WANTS_CLEAN`" | tee -a "$LOG"
echo -e " BUILD: `b2a $WANTS_BUILD`" | tee -a "$LOG"
echo -e " CONFIG: `b2a $WANTS_CONFIG`" | tee -a "$LOG"
echo -e " NINJA: `b2a $WANTS_NINJA`" | tee -a "$LOG"
echo -e " VSCODE: `b2a $WANTS_VSCODE`" | tee -a "$LOG"
echo -e " COMPILER CACHE: `b2a $WANTS_CACHE`" | tee -a "$LOG"
echo -e " PASSTHRU: $LL_ARGS_PASSTHRU" | tee -a "$LOG"
echo -e " BTYPE: $BTYPE" | tee -a "$LOG"
if [ $TARGET_PLATFORM == "linux" -o $TARGET_PLATFORM == "darwin" ] ; then
echo -e " JOBS: $JOBS" | tee -a "$LOG"
fi
echo -e " Logging to $LOG"
if [ $TARGET_PLATFORM == "windows" ]
then
if [ -z "${AUTOBUILD_VSVER}" ]
then
echo "AUTOBUILD_VSVER not set, this can lead to Autobuild picking a higher VS version than desired."
echo "If you see this happen you should set the variable to e.g. 150 for Visual Studio 2017."
fi
echo "Setting environment variables for Visual Studio..."
if [ "$OSTYPE" = "cygwin" ] ; then
export AUTOBUILD_EXEC="$(cygpath -u $AUTOBUILD)"
fi
if [ -z "$AUTOBUILD_EXEC" ]
then
export AUTOBUILD_EXEC=`which autobuild`
fi
# load autobuild provided shell functions and variables
eval "$("$AUTOBUILD_EXEC" source_environment)"
# vsvars is needed for determing path to VS runtime redist files in Copy3rdPartyLibs.cmake
load_vsvars
fi
if [ -z "$AUTOBUILD_VARIABLES_FILE" ]
then
echo "AUTOBUILD_VARIABLES_FILE not set."
echo "In order to run autobuild it needs to be set to point to a correct variables file."
exit 1
fi
if [ $TARGET_PLATFORM == "windows" ] ; then
FIND=/usr/bin/find
else
FIND=find
fi
CHANNEL_SIMPLE="$CHANNEL"
if [ -z $CHANNEL ] ; then
if [ $TARGET_PLATFORM == "darwin" ] ; then
CHANNEL="private-`hostname -s` "
else
CHANNEL="private-`hostname`"
fi
else
CHANNEL=`echo $CHANNEL | sed -e "s/[^a-zA-Z0-9\-]*//g"` # strip out difficult characters from channel
fi
CHANNEL="Puma-$CHANNEL"
if [ \( $WANTS_CLEAN -eq $TRUE \) -a \( $WANTS_BUILD -eq $FALSE \) ] ; then
echo "Cleaning $TARGET_PLATFORM...."
wdir=`pwd`
pushd ..
if [ $TARGET_PLATFORM == "darwin" ] ; then
if [ "${AUTOBUILD_ADDRSIZE}" == "64" ]
then
rm -rf build-darwin-x86_64/*
mkdir -p build-darwin-x86_64/logs
else
rm -rf build-darwin-i386/*
mkdir -p build-darwin-i386/logs
fi
elif [ $TARGET_PLATFORM == "windows" ] ; then
rm -rf build-vc${AUTOBUILD_VSVER:-150}-${AUTOBUILD_ADDRSIZE}
mkdir -p build-vc${AUTOBUILD_VSVER:-150}-${AUTOBUILD_ADDRSIZE}/logs
elif [ $TARGET_PLATFORM == "linux" ] ; then
if [ "${AUTOBUILD_ADDRSIZE}" == "64" ]
then
rm -rf build-linux-x86_64/*
mkdir -p build-linux-x86_64/logs
else
rm -rf build-linux-i686/*
mkdir -p build-linux-i686/logs
fi
fi
popd
fi
if [ \( $WANTS_VERSION -eq $TRUE \) -o \( $WANTS_CONFIG -eq $TRUE \) ] ; then
echo "Versioning..."
pushd ..
if [ -d .git ]
then
buildVer=`git rev-list --count HEAD`
else
buildVer=`hg summary | head -1 | cut -d " " -f 2 | cut -d : -f 1 | grep "[0-9]*"`
fi
export revision=${buildVer}
majorVer=`cat indra/newview/VIEWER_VERSION.txt | cut -d "." -f 1`
minorVer=`cat indra/newview/VIEWER_VERSION.txt | cut -d "." -f 2`
patchVer=`cat indra/newview/VIEWER_VERSION.txt | cut -d "." -f 3`
gitHash=`git describe --always --exclude '*'`
echo "Channel : ${CHANNEL}"
echo "Version : ${majorVer}.${minorVer}.${patchVer}.${buildVer} [${gitHash}]"
GITHASH=-DVIEWER_VERSION_GITHASH=\"${gitHash}\"
popd
fi
if [ $WANTS_CONFIG -eq $TRUE ] ; then
echo "Configuring $TARGET_PLATFORM..."
if [ $WANTS_FMODSTUDIO -eq $TRUE ] ; then
FMODSTUDIO="-DUSE_FMODSTUDIO:BOOL=ON"
else
FMODSTUDIO="-DUSE_FMODSTUDIO:BOOL=OFF"
fi
if [ $WANTS_OPENAL -eq $TRUE ] ; then
OPENAL="-DOPENAL:BOOL=ON"
else
OPENAL="-DOPENAL:BOOL=OFF"
fi
if [ $WANTS_OPENSIM -eq $TRUE ] ; then
OPENSIM="-DOPENSIM:BOOL=ON"
else
OPENSIM="-DOPENSIM:BOOL=OFF"
fi
if [ $WANTS_SINGLEGRID -eq $TRUE ] ; then
SINGLEGRID="-DSINGLEGRID:BOOL=ON -DSINGLEGRID_URI:STRING=$SINGLEGRID_URI"
else
SINGLEGRID="-DSINGLEGRID:BOOL=OFF"
fi
if [ $WANTS_HAVOK -eq $TRUE ] ; then
HAVOK="-DHAVOK_TPV:BOOL=ON"
else
HAVOK="-DHAVOK_TPV:BOOL=OFF"
fi
if [ $WANTS_AVX -eq $TRUE ] ; then
AVX_OPTIMIZATION="-DUSE_AVX_OPTIMIZATION:BOOL=ON"
else
AVX_OPTIMIZATION="-DUSE_AVX_OPTIMIZATION:BOOL=OFF"
fi
if [ $WANTS_AVX2 -eq $TRUE ] ; then
AVX2_OPTIMIZATION="-DUSE_AVX2_OPTIMIZATION:BOOL=ON"
else
AVX2_OPTIMIZATION="-DUSE_AVX2_OPTIMIZATION:BOOL=OFF"
fi
if [ $WANTS_TRACY -eq $TRUE ] ; then
TRACY_PROFILER="-DUSE_TRACY:BOOL=ON"
else
TRACY_PROFILER="-DUSE_TRACY:BOOL=OFF"
fi
if [ $WANTS_TESTBUILD -eq $TRUE ] ; then
TESTBUILD="-DTESTBUILD:BOOL=ON -DTESTBUILDPERIOD:STRING=$TESTBUILD_PERIOD"
else
TESTBUILD="-DTESTBUILD:BOOL=OFF"
fi
if [ $WANTS_PACKAGE -eq $TRUE ] ; then
PACKAGE="-DPACKAGE:BOOL=ON"
# Also delete easy-to-copy resource files, insuring that we properly refresh resoures from the source tree
if [ -d skins ] ; then
echo "Removing select previously packaged resources, they will refresh at build time"
for subdir in skins app_settings fs_resources ; do
for resourcedir in `$FIND . -type d -name $subdir` ; do
rm -rf $resourcedir ;
done
done
fi
else
PACKAGE="-DPACKAGE:BOOL=OFF"
fi
if [ $WANTS_CRASHREPORTING -eq $TRUE ] ; then
if [ $TARGET_PLATFORM == "windows" ] ; then
BUILD_DIR=`cygpath -w $(pwd)`
else
BUILD_DIR=`pwd`
fi
# This name is consumed by indra/newview/CMakeLists.txt
if [ $TARGET_PLATFORM == "linux" ] ; then
VIEWER_SYMBOL_FILE="${BUILD_DIR}/newview/puma-symbols-${TARGET_PLATFORM}-${AUTOBUILD_ADDRSIZE}.tar.bz2"
else
VIEWER_SYMBOL_FILE="${BUILD_DIR}/newview/$BTYPE/puma-symbols-${TARGET_PLATFORM}-${AUTOBUILD_ADDRSIZE}.tar.bz2"
fi
CRASH_REPORTING="-DRELEASE_CRASH_REPORTING=ON"
if [ ! -z $CHANNEL_SIMPLE ]
then
CRASH_REPORTING="$CRASH_REPORTING -DUSE_BUGSPLAT=On -DBUGSPLAT_DB=puma_"`echo $CHANNEL_SIMPLE | tr [:upper:] [:lower:] | sed -e 's/x64//' | sed 's/[^A-Za-z0-9]//g'`
fi
else
CRASH_REPORTING="-DRELEASE_CRASH_REPORTING:BOOL=OFF"
fi
CHANNEL="-DVIEWER_CHANNEL:STRING=$CHANNEL"
#make sure log directory exists.
if [ ! -d "logs" ] ; then
echo "Creating logging dir `pwd`/logs"
mkdir -p "logs"
fi
CMAKE_ARCH=""
if [ $TARGET_PLATFORM == "darwin" ] ; then
TARGET="Xcode"
elif [ \( $TARGET_PLATFORM == "linux" \) ] ; then
TARGET="Unix Makefiles"
if [ $WANTS_VSCODE -eq $TRUE ] ; then
VSCODE_FLAGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On"
ROOT_DIR=$(dirname $(dirname $(readlink -f $0)))
if [ -d ${ROOT_DIR}/vscode_template/ ]
then
test -d "${ROOT_DIR}/.vscode" || mkdir "${ROOT_DIR}/.vscode"
cp -n "${ROOT_DIR}/vscode_template/"* "${ROOT_DIR}/.vscode/"
fi
fi
elif [ \( $TARGET_PLATFORM == "windows" \) ] ; then
TARGET="${AUTOBUILD_WIN_CMAKE_GEN}"
if [ $AUTOBUILD_ADDRSIZE == 32 ]
then
CMAKE_ARCH="-A Win32"
fi
UNATTENDED="-DUNATTENDED=ON"
fi
if [ $WANTS_NINJA -eq $TRUE ] ; then
TARGET="Ninja"
fi
CACHE_OPT=""
if [ $WANTS_CACHE -eq $TRUE ]
then
if [ `which ccache 2>/dev/null` ]
then
echo "Found ccache"
CACHE_OPT="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
fi
if [ `which buildcache 2>/dev/null` ]
then
echo "Found buildcache"
CACHE_OPT="-DCMAKE_C_COMPILER_LAUNCHER=buildcache -DCMAKE_CXX_COMPILER_LAUNCHER=buildcache"
fi
fi
cmake -G "$TARGET" $CMAKE_ARCH ../indra $CHANNEL ${GITHASH} $FMODSTUDIO $OPENAL $OPENSIM $SINGLEGRID $HAVOK $AVX_OPTIMIZATION $AVX2_OPTIMIZATION $TRACY_PROFILER $TESTBUILD $PACKAGE \
$UNATTENDED -DLL_TESTS:BOOL=OFF -DADDRESS_SIZE:STRING=$AUTOBUILD_ADDRSIZE -DCMAKE_BUILD_TYPE:STRING=$BTYPE $CACHE_OPT \
$CRASH_REPORTING -DVIEWER_SYMBOL_FILE:STRING="${VIEWER_SYMBOL_FILE:-}" $LL_ARGS_PASSTHRU ${VSCODE_FLAGS:-} | tee "$LOG"
if [ $TARGET_PLATFORM == "windows" -a $USE_VSTOOL -eq $TRUE ] ; then
echo "Setting startup project via vstool"
../indra/tools/vstool/VSTool.exe --solution Puma.sln --startup puma-bin --workingdir puma-bin "..\\..\\indra\\newview" --config $BTYPE
fi
# Check the return code of the build command
if [ $? -ne 0 ]; then
echo "Configure failed!"
exit 1
fi
fi
if [ $WANTS_BUILD -eq $TRUE ] ; then
echo "Building $TARGET_PLATFORM..."
if [ $TARGET_PLATFORM == "darwin" ] ; then
if [ $JOBS == "0" ] ; then
JOBS=""
else
JOBS="-jobs $JOBS"
fi
xcodebuild -configuration $BTYPE -project puma.xcodeproj $JOBS 2>&1 | tee -a "$LOG"
elif [ $TARGET_PLATFORM == "linux" ] ; then
if [ $JOBS == "0" ] ; then
JOBS=`cat /proc/cpuinfo | grep processor | wc -l`
echo $JOBS
fi
if [ $WANTS_NINJA -eq $TRUE ] ; then
ninja -j $JOBS | tee -a "$LOG"
else
make -j $JOBS | tee -a "$LOG"
fi
elif [ $TARGET_PLATFORM == "windows" ] ; then
msbuild.exe Puma.sln -p:Configuration=${BTYPE} -flp:LogFile="logs\\PumaBuild_win-${AUTOBUILD_ADDRSIZE}.log" \
-flp1:"errorsonly;LogFile=logs\\PumaBuild_win-${AUTOBUILD_ADDRSIZE}.err" -p:Platform=${AUTOBUILD_WIN_VSPLATFORM} -t:Build -p:useenv=true \
-verbosity:normal -toolsversion:Current -p:"VCBuildAdditionalOptions= /incremental"
fi
# Check the return code of the build command
if [ $? -ne 0 ]; then
echo "Build failed!"
exit 1
fi
fi
echo "finished"
exit 0
+730
View File
@@ -0,0 +1,730 @@
#!/usr/bin/env python3
"""\
@file anim_tool.py
@author Brad Payne, Nat Goodspeed
@date 2015-09-15
@brief This module contains tools for manipulating the .anim files supported
for Second Life animation upload. Note that this format is unrelated
to any non-Second Life formats of the same name.
This code is a Python translation of the logic in
LLKeyframeMotion::serialize() and deserialize():
https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1864
https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1220
save that there is no support for old-style .anim files, permitting
simpler code.
$LicenseInfo:firstyear=2015&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2015, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import math
import os
import random
from io import StringIO
import struct
import sys
from xml.etree import ElementTree
class Error(Exception):
pass
class BadFormat(Error):
"""
Something went wrong trying to read the specified .anim file.
"""
pass
class ExtraneousData(BadFormat):
"""
Specifically, the .anim file in question contains more data than needed.
This could happen if the file isn't a .anim at all, and it 'just happens'
to read properly otherwise -- e.g. a block of all zero bytes could look
like empty name strings, empty arrays etc. That could be a legitimate
error -- or it could be due to a sloppy tool. Break this exception out
separately so caller can distinguish if desired.
"""
pass
U16MAX = 65535
# One Over U16MAX, for scaling
OOU16MAX = 1.0/float(U16MAX)
LL_MAX_PELVIS_OFFSET = 5.0
class FilePacker(object):
def __init__(self):
self.buffer = StringIO()
def write(self,filename):
with open(filename,"wb") as f:
f.write(self.buffer.getvalue())
def pack(self,fmt,*args):
buf = struct.pack(fmt, *args)
self.buffer.write(buf)
def pack_string(self,str,size=0):
# If size == 0, caller doesn't care, just wants a terminating nul byte
size = size or (len(str) + 1)
# Nonzero size means a fixed-length field. If the passed string (plus
# its terminating nul) exceeds that fixed length, we'll have to
# truncate. But make sure we still leave room for the final nul byte!
str = str[:size-1]
# Now pad what's left of str out to 'size' with nul bytes.
buf = str + ("\000" * (size-len(str)))
self.buffer.write(buf)
class FileUnpacker(object):
def __init__(self, filename):
with open(filename,"rb") as f:
self.buffer = f.read()
self.offset = 0
def unpack(self,fmt):
result = struct.unpack_from(fmt, self.buffer, self.offset)
self.offset += struct.calcsize(fmt)
return result
def unpack_string(self, size=0):
# Nonzero size means we must consider exactly the next 'size'
# characters in self.buffer.
if size:
self.offset += size
# but stop at the first nul byte
return self.buffer[self.offset-size:self.offset].split("\000", 1)[0]
# Zero size means consider everything until the next nul character.
result = self.buffer[self.offset:].split("\000", 1)[0]
# don't forget to skip the nul byte too
self.offset += len(result) + 1
return result
# translated from the C++ version in lldefs.h
def llclamp(a, minval, maxval):
if a<minval:
return minval
if a>maxval:
return maxval
return a
# translated from the C++ version in llquantize.h
def F32_to_U16(val, lower, upper):
val = llclamp(val, lower, upper);
# make sure that the value is positive and normalized to <0, 1>
val -= lower;
val /= (upper - lower);
# return the U16
return int(math.floor(val*U16MAX))
# translated from the C++ version in llquantize.h
def U16_to_F32(ival, lower, upper):
if ival < 0 or ival > U16MAX:
raise ValueError("U16 out of range: %s" % ival)
val = ival*OOU16MAX
delta = (upper - lower)
val *= delta
val += lower
max_error = delta*OOU16MAX;
# make sure that zeroes come through as zero
if abs(val) < max_error:
val = 0.0
return val;
class RotKey(object):
def __init__(self, time, duration, rot):
"""
This constructor instantiates a RotKey object from scratch, as it
were, converting from float time to time_short.
"""
self.time = time
self.time_short = F32_to_U16(time, 0.0, duration) \
if time is not None else None
self.rotation = rot
@staticmethod
def unpack(duration, fup):
"""
This staticmethod constructs a RotKey by loadingfrom a FileUnpacker.
"""
# cheat the other constructor
this = RotKey(None, None, None)
# load time_short directly from the file
(this.time_short, ) = fup.unpack("<H")
# then convert to float time
this.time = U16_to_F32(this.time_short, 0.0, duration)
# convert each coordinate of the rotation from short to float
(x,y,z) = fup.unpack("<HHH")
this.rotation = [U16_to_F32(i, -1.0, 1.0) for i in (x,y,z)]
return this
def dump(self, f):
print(" rot_key: t %.3f" % self.time,"st",self.time_short,"rot",",".join("%.3f" % f for f in self.rotation), file=f)
def pack(self, fp):
fp.pack("<H",self.time_short)
(x,y,z) = [F32_to_U16(v, -1.0, 1.0) for v in self.rotation]
fp.pack("<HHH",x,y,z)
class PosKey(object):
def __init__(self, time, duration, pos):
"""
This constructor instantiates a PosKey object from scratch, as it
were, converting from float time to time_short.
"""
self.time = time
self.time_short = F32_to_U16(time, 0.0, duration) \
if time is not None else None
self.position = pos
@staticmethod
def unpack(duration, fup):
"""
This staticmethod constructs a PosKey by loadingfrom a FileUnpacker.
"""
# cheat the other constructor
this = PosKey(None, None, None)
# load time_short directly from the file
(this.time_short, ) = fup.unpack("<H")
# then convert to float time
this.time = U16_to_F32(this.time_short, 0.0, duration)
# convert each coordinate of the rotation from short to float
(x,y,z) = fup.unpack("<HHH")
this.position = [U16_to_F32(i, -LL_MAX_PELVIS_OFFSET, LL_MAX_PELVIS_OFFSET)
for i in (x,y,z)]
return this
def dump(self, f):
print(" pos_key: t %.3f" % self.time,"pos ",",".join("%.3f" % f for f in self.position), file=f)
def pack(self, fp):
fp.pack("<H",self.time_short)
(x,y,z) = [F32_to_U16(v, -LL_MAX_PELVIS_OFFSET, LL_MAX_PELVIS_OFFSET) for v in self.position]
fp.pack("<HHH",x,y,z)
class Constraint(object):
@staticmethod
def unpack(duration, fup):
this = Constraint()
(this.chain_length, this.constraint_type) = fup.unpack("<BB")
this.source_volume = fup.unpack_string(16)
this.source_offset = fup.unpack("<fff")
this.target_volume = fup.unpack_string(16)
this.target_offset = fup.unpack("<fff")
this.target_dir = fup.unpack("<fff")
(this.ease_in_start, this.ease_in_stop, this.ease_out_start, this.ease_out_stop) = \
fup.unpack("<ffff")
return this
def pack(self, fp):
fp.pack("<BB", self.chain_length, self.constraint_type)
fp.pack_string(self.source_volume, 16)
fp.pack("<fff", *self.source_offset)
fp.pack_string(self.target_volume, 16)
fp.pack("<fff", *self.target_offset)
fp.pack("<fff", *self.target_dir)
fp.pack("<ffff", self.ease_in_start, self.ease_in_stop,
self.ease_out_start, self.ease_out_stop)
def dump(self, f):
print(" constraint:", file=f)
print(" chain_length",self.chain_length, file=f)
print(" constraint_type",self.constraint_type, file=f)
print(" source_volume",self.source_volume, file=f)
print(" source_offset",self.source_offset, file=f)
print(" target_volume",self.target_volume, file=f)
print(" target_offset",self.target_offset, file=f)
print(" target_dir",self.target_dir, file=f)
print(" ease_in_start",self.ease_in_start, file=f)
print(" ease_in_stop",self.ease_in_stop, file=f)
print(" ease_out_start",self.ease_out_start, file=f)
print(" ease_out_stop",self.ease_out_stop, file=f)
class Constraints(object):
@staticmethod
def unpack(duration, fup):
this = Constraints()
(num_constraints, ) = fup.unpack("<i")
this.constraints = [Constraint.unpack(duration, fup)
for i in range(num_constraints)]
return this
def pack(self, fp):
fp.pack("<i",len(self.constraints))
for c in self.constraints:
c.pack(fp)
def dump(self, f):
print("constraints:",len(self.constraints), file=f)
for c in self.constraints:
c.dump(f)
class PositionCurve(object):
def __init__(self):
self.keys = []
def is_static(self):
if self.keys:
k0 = self.keys[0]
for k in self.keys:
if k.position != k0.position:
return False
return True
@staticmethod
def unpack(duration, fup):
this = PositionCurve()
(num_pos_keys, ) = fup.unpack("<i")
this.keys = [PosKey.unpack(duration, fup)
for k in range(num_pos_keys)]
return this
def pack(self, fp):
fp.pack("<i",len(self.keys))
for k in self.keys:
k.pack(fp)
def dump(self, f):
print(" position_curve:", file=f)
print(" num_pos_keys", len(self.keys), file=f)
for k in self.keys:
k.dump(f)
class RotationCurve(object):
def __init__(self):
self.keys = []
def is_static(self):
if self.keys:
k0 = self.keys[0]
for k in self.keys:
if k.rotation != k0.rotation:
return False
return True
@staticmethod
def unpack(duration, fup):
this = RotationCurve()
(num_rot_keys, ) = fup.unpack("<i")
this.keys = [RotKey.unpack(duration, fup)
for k in range(num_rot_keys)]
return this
def pack(self, fp):
fp.pack("<i",len(self.keys))
for k in self.keys:
k.pack(fp)
def dump(self, f):
print(" rotation_curve:", file=f)
print(" num_rot_keys", len(self.keys), file=f)
for k in self.keys:
k.dump(f)
class JointInfo(object):
def __init__(self, name, priority):
self.joint_name = name
self.joint_priority = priority
self.rotation_curve = RotationCurve()
self.position_curve = PositionCurve()
@staticmethod
def unpack(duration, fup):
this = JointInfo(None, None)
this.joint_name = fup.unpack_string()
(this.joint_priority, ) = fup.unpack("<i")
this.rotation_curve = RotationCurve.unpack(duration, fup)
this.position_curve = PositionCurve.unpack(duration, fup)
return this
def pack(self, fp):
fp.pack_string(self.joint_name)
fp.pack("<i", self.joint_priority)
self.rotation_curve.pack(fp)
self.position_curve.pack(fp)
def dump(self, f):
print("joint:", file=f)
print(" joint_name:",self.joint_name, file=f)
print(" joint_priority:",self.joint_priority, file=f)
self.rotation_curve.dump(f)
self.position_curve.dump(f)
class Anim(object):
def __init__(self, filename=None, verbose=False):
# set this FIRST as it's consulted by read() and unpack()
self.verbose = verbose
if filename:
self.read(filename)
def read(self, filename):
fup = FileUnpacker(filename)
try:
self.unpack(fup)
except struct.error as err:
raise BadFormat("error reading %s: %s" % (filename, err))
# By the end of streaming data in from our FileUnpacker, we should
# have consumed the entire thing. If there's excess data, it's
# entirely possible that this is a garbage file that happens to
# resemble a valid degenerate .anim file, e.g. with zero counts of
# things.
if fup.offset != len(fup.buffer):
raise ExtraneousData("extraneous data in %s; is it really a Linden .anim file?" %
filename)
# various validity checks could be added - see LLKeyframeMotion::deserialize()
def unpack(self,fup):
(self.version, self.sub_version, self.base_priority, self.duration) = fup.unpack("@HHhf")
if self.version == 0 and self.sub_version == 1:
self.old_version = True
raise BadFormat("old version not supported")
elif self.version == 1 and self.sub_version == 0:
self.old_version = False
else:
raise BadFormat("Bad combination of version, sub_version: %d %d" % (self.version, self.sub_version))
# Also consult BVH conversion code for stricter checks
# C++ deserialize() checks self.base_priority against
# LLJoint::ADDITIVE_PRIORITY and LLJoint::USE_MOTION_PRIORITY,
# possibly sets self.max_priority
# checks self.duration against MAX_ANIM_DURATION !!
# checks self.emote_name != str(self.ID)
# checks self.hand_pose against LLHandMotion::NUM_HAND_POSES !!
# checks 0 < num_joints <= LL_CHARACTER_MAX_JOINTS (no need --
# validate names)
# checks each joint_name neither "mScreen" nor "mRoot" ("attempted to
# animate special joint") !!
# checks each joint_name can be found in mCharacter
# checks each joint_priority >= LLJoint::USE_MOTION_PRIORITY
# tracks max observed joint_priority, excluding USE_MOTION_PRIORITY
# checks each 0 <= RotKey.time <= self.duration !!
# checks each RotKey.rotation.isFinite() !!
# checks each PosKey.position.isFinite() !!
# checks 0 <= num_constraints <= MAX_CONSTRAINTS !!
# checks each Constraint.chain_length <= num_joints
# checks each Constraint.constraint_type < NUM_CONSTRAINT_TYPES !!
# checks each Constraint.source_offset.isFinite() !!
# checks each Constraint.target_offset.isFinite() !!
# checks each Constraint.target_dir.isFinite() !!
# from https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1812 :
# find joint to which each Constraint's collision volume is attached;
# for each link in Constraint.chain_length, walk to joint's parent,
# find that parent in list of joints, set its index in index list
self.emote_name = fup.unpack_string()
(self.loop_in_point, self.loop_out_point, self.loop,
self.ease_in_duration, self.ease_out_duration, self.hand_pose, num_joints) = \
fup.unpack("@ffiffII")
self.joints = [JointInfo.unpack(self.duration, fup)
for j in range(num_joints)]
if self.verbose:
for joint_info in self.joints:
print("unpacked joint",joint_info.joint_name)
self.constraints = Constraints.unpack(self.duration, fup)
self.buffer = fup.buffer
def pack(self, fp):
fp.pack("@HHhf", self.version, self.sub_version, self.base_priority, self.duration)
fp.pack_string(self.emote_name, 0)
fp.pack("@ffiffII", self.loop_in_point, self.loop_out_point, self.loop,
self.ease_in_duration, self.ease_out_duration, self.hand_pose, len(self.joints))
for j in self.joints:
j.pack(fp)
self.constraints.pack(fp)
def dump(self, filename="-"):
if filename=="-":
f = sys.stdout
else:
f = open(filename,"w")
print("versions: ", self.version, self.sub_version, file=f)
print("base_priority: ", self.base_priority, file=f)
print("duration: ", self.duration, file=f)
print("emote_name: ", self.emote_name, file=f)
print("loop_in_point: ", self.loop_in_point, file=f)
print("loop_out_point: ", self.loop_out_point, file=f)
print("loop: ", self.loop, file=f)
print("ease_in_duration: ", self.ease_in_duration, file=f)
print("ease_out_duration: ", self.ease_out_duration, file=f)
print("hand_pose", self.hand_pose, file=f)
print("num_joints", len(self.joints), file=f)
for j in self.joints:
j.dump(f)
self.constraints.dump(f)
def write(self, filename):
fp = FilePacker()
self.pack(fp)
fp.write(filename)
def write_src_data(self, filename):
print("write file",filename)
with open(filename,"wb") as f:
f.write(self.buffer)
def find_joint(self, name):
joints = [j for j in self.joints if j.joint_name == name]
if joints:
return joints[0]
else:
return None
def add_joint(self, name, priority):
if not self.find_joint(name):
self.joints.append(JointInfo(name, priority))
def delete_joint(self, name):
j = self.find_joint(name)
if j:
if self.verbose:
print("removing joint", name)
self.joints.remove(j)
else:
if self.verbose:
print("joint not found to remove", name)
def summary(self):
nj = len(self.joints)
nz = len([j for j in self.joints if j.joint_priority > 0])
nstatic = len([j for j in self.joints
if j.rotation_curve.is_static()
and j.position_curve.is_static()])
print("summary: %d joints, non-zero priority %d, static %d" % (nj, nz, nstatic))
def add_pos(self, joint_names, positions):
js = [joint for joint in self.joints if joint.joint_name in joint_names]
for j in js:
if self.verbose:
print("adding positions",j.joint_name,positions)
j.joint_priority = 4
j.position_curve.keys = [PosKey(self.duration * i / (len(positions) - 1),
self.duration,
pos)
for i,pos in enumerate(positions)]
def add_rot(self, joint_names, rotations):
js = [joint for joint in self.joints if joint.joint_name in joint_names]
for j in js:
print("adding rotations",j.joint_name)
j.joint_priority = 4
j.rotation_curve.keys = [RotKey(self.duration * i / (len(rotations) - 1),
self.duration,
rot)
for i,rot in enumerate(rotations)]
def twistify(anim, joint_names, rot1, rot2):
js = [joint for joint in anim.joints if joint.joint_name in joint_names]
for j in js:
print("twisting",j.joint_name)
print(len(j.rotation_curve.keys))
j.joint_priority = 4
# Set the joint(s) to rot1 at time 0, rot2 at the full duration.
j.rotation_curve.keys = [
RotKey(0.0, anim.duration, rot1),
RotKey(anim.duration, anim.duration, rot2)]
def float_triple(arg):
vals = arg.split()
if len(vals)==3:
return [float(x) for x in vals]
else:
raise ValueError("arg %s does not resolve to a float triple" % arg)
def get_joint_by_name(tree,name):
if tree is None:
return None
matches = [elt for elt in tree.getroot().iter()
if elt.get("name")==name
and elt.tag in ["bone", "collision_volume", "attachment_point"]]
if len(matches)==1:
return matches[0]
elif len(matches)>1:
print("multiple matches for name",name)
return None
else:
return None
def get_elt_pos(elt):
if elt.get("pos"):
return float_triple(elt.get("pos"))
elif elt.get("position"):
return float_triple(elt.get("position"))
else:
return (0.0, 0.0, 0.0)
def resolve_joints(names, skel_tree, lad_tree, no_hud=False):
print("resolve joints, no_hud is",no_hud)
if skel_tree and lad_tree:
all_elts = [elt for elt in skel_tree.getroot().iter()]
all_elts.extend([elt for elt in lad_tree.getroot().iter()])
matches = set()
for elt in all_elts:
if elt.get("name") is None:
continue
#print elt.get("name"),"hud",elt.get("hud")
if no_hud and elt.get("hud"):
#print "skipping hud joint", elt.get("name")
continue
if elt.get("name") in names or elt.tag in names:
matches.add(elt.get("name"))
return list(matches)
else:
return names
def main(*argv):
import argparse
# default search location for config files is defined relative to
# the script location; assuming they live in the same viewer repo
# Use sys.argv[0] because (a) this script lives where it lives regardless
# of what our caller passes and (b) we don't expect our caller to pass the
# script name anyway.
pathname = os.path.dirname(sys.argv[0])
# we're in scripts/content_tools; hop back to base of repository clone
path_to_skel = os.path.join(os.path.abspath(pathname),os.pardir,os.pardir,
"indra","newview","character")
parser = argparse.ArgumentParser(description="process SL animations")
parser.add_argument("--verbose", help="verbose flag", action="store_true")
parser.add_argument("--dump", help="dump to stdout", action="store_true")
parser.add_argument("--use_aliases", help="use alias names for bones", action="store_true")
parser.add_argument("--rot", help="specify sequence of rotations", type=float_triple, nargs="+")
parser.add_argument("--rand_pos", help="request NUM random positions (default %(default)s)",
metavar="NUM", type=int, default=2)
parser.add_argument("--reset_pos", help="request original positions", action="store_true")
parser.add_argument("--pos", help="specify sequence of positions", type=float_triple, nargs="+")
parser.add_argument("--duration", help="specify duration", type=float)
parser.add_argument("--loop_in", help="specify loop in time", type=float)
parser.add_argument("--loop_out", help="specify loop out time", type=float)
parser.add_argument("--num_pos", help="number of positions to create", type=int, default=2)
parser.add_argument("--delete_joints", help="specify joints to be deleted", nargs="+",
metavar="JOINT")
parser.add_argument("--joints", help="specify joints to be added or modified", nargs="+",
metavar="JOINT")
parser.add_argument("--summary", help="print summary of the output animation", action="store_true")
parser.add_argument("--skel", help="name of the avatar_skeleton file (default %(default)s)",
default=os.path.join(path_to_skel,"avatar_skeleton.xml"),
metavar="FILEPATH")
parser.add_argument("--lad", help="name of the avatar_lad file (default %(default)s)",
default=os.path.join(path_to_skel,"avatar_lad.xml"),
metavar="FILEPATH")
parser.add_argument("--set_version", nargs=2, type=int,
help="set version and sub-version to specified values",
metavar=("VERSION", "SUB-VERSION"))
parser.add_argument("--no_hud", help="omit hud joints from list of attachments", action="store_true")
parser.add_argument("--base_priority", help="set base priority", type=int)
parser.add_argument("--joint_priority", help="set joint priority for all joints", type=int)
parser.add_argument("--force_joints", help="don't check validity of joint names", action="store_true")
parser.add_argument("infilename", help="name of a .anim file to input")
parser.add_argument("outfilename", nargs="?", help="name of a .anim file to output")
args = parser.parse_args(argv)
print("anim_tool.py: " + " ".join(argv))
print("dump is", args.dump)
print("infilename",args.infilename,"outfilename",args.outfilename)
print("rot",args.rot)
print("pos",args.pos)
print("joints",args.joints)
anim = Anim(args.infilename, args.verbose)
skel_tree = None
lad_tree = None
joints = []
if args.skel:
skel_tree = ElementTree.parse(args.skel)
if skel_tree is None:
raise Error("failed to parse " + args.skel)
if args.lad:
lad_tree = ElementTree.parse(args.lad)
if lad_tree is None:
raise Error("failed to parse " + args.lad)
if args.joints:
if args.force_joints:
joints = args.joints
else:
joints = resolve_joints(args.joints, skel_tree, lad_tree, args.no_hud)
if args.use_aliases:
joints = map(lambda name: "avatar_" + name, joints)
if args.verbose:
print("joints resolved to",joints)
for name in joints:
anim.add_joint(name,0)
if args.delete_joints:
for name in args.delete_joints:
anim.delete_joint(name)
if joints and args.rot:
anim.add_rot(joints, args.rot)
if joints and args.pos:
anim.add_pos(joints, args.pos)
if joints and args.rand_pos:
# pick a random sequence of positions for each joint specified
for joint in joints:
# generate a list of rand_pos triples
pos_array = [tuple(random.uniform(-1,1) for i in range(3))
for j in range(args.rand_pos)]
# close the loop by cycling back to the first entry
pos_array.append(pos_array[0])
anim.add_pos([joint], pos_array)
if joints and args.reset_pos:
for joint in joints:
elt = get_joint_by_name(skel_tree,joint) or get_joint_by_name(lad_tree,joint)
if elt is not None:
anim.add_pos([joint], 2*[get_elt_pos(elt)])
else:
print("no elt or no pos data for",joint)
if args.set_version:
anim.version, anim.sub_version = args.set_version
if args.base_priority is not None:
print("set base priority",args.base_priority)
anim.base_priority = args.base_priority
# --joint_priority sets priority for ALL joints, not just the explicitly-
# specified ones
if args.joint_priority is not None:
print("set joint priority",args.joint_priority)
for joint in anim.joints:
joint.joint_priority = args.joint_priority
if args.duration is not None:
print("set duration",args.duration)
anim.duration = args.duration
if args.loop_in is not None:
print("set loop_in",args.loop_in)
anim.loop_in_point = args.loop_in
if args.loop_out is not None:
print("set loop_out",args.loop_out)
anim.loop_out_point = args.loop_out
if args.dump:
anim.dump("-")
if args.summary:
anim.summary()
if args.outfilename:
anim.write(args.outfilename)
if __name__ == "__main__":
try:
sys.exit(main(*sys.argv[1:]))
except Error as err:
sys.exit("%s: %s" % (err.__class__.__name__, err))
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""\
This module contains tools for comparing files output by LLVOAvatar::dumpArchetypeXML
$LicenseInfo:firstyear=2016&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2016, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
from lxml import etree
from itertools import chain
def node_key(e):
if e.tag == "param":
return e.tag + " " + e.get("id")
if e.tag == "texture":
return e.tag + " " + e.get("te")
if e.get("name"):
return e.tag + " " + e.get("name")
return None
def compare_matched_nodes(key,items,summary):
tags = list(set([e.tag for e in items]))
if len(tags) != 1:
print("different tag types for key",key)
summary.setdefault("tag_mismatch",0)
summary["tag_mismatch"] += 1
return
all_attrib = list(set(chain.from_iterable([list(e.attrib.keys()) for e in items])))
#print key,"all_attrib",all_attrib
for attr in all_attrib:
vals = [e.get(attr) for e in items]
#print "key",key,"attr",attr,"vals",vals
if len(set(vals)) != 1:
print(key,"- attr",attr,"multiple values",vals)
summary.setdefault("attr",{})
summary["attr"].setdefault(attr,0)
summary["attr"][attr] += 1
def compare_trees(file_trees):
print("compare_trees")
summary = {}
all_keys = list(set([node_key(e) for tree in file_trees for e in tree.getroot().iter() if node_key(e)]))
#print "keys",all_keys
tree_nodes = []
for i,tree in enumerate(file_trees):
nodes = dict((node_key(e),e) for e in tree.getroot().iter() if node_key(e))
tree_nodes.append(nodes)
for key in sorted(all_keys):
items = []
for nodes in tree_nodes:
if not key in nodes:
print("file",i,"missing item for key",key)
summary.setdefault("missing",0)
summary["missing"] += 1
else:
items.append(nodes[key])
compare_matched_nodes(key,items,summary)
print("Summary:")
print(summary)
def dump_appearance_params(tree):
vals = []
for e in tree.getroot().iter():
if e.tag == "param":
g = int(e.get("group"))
if g in [0,3]:
vals.append("{" + e.get("id") + "," +e.get("u8") + "}")
#print e.get("id"), e.get("name"), e.get("group"), e.get("u8")
if len(vals)==253:
print(", ".join(vals))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="compare avatar XML archetype files")
parser.add_argument("--verbose", help="verbose flag", action="store_true")
parser.add_argument("--compare", help="compare flag", action="store_true")
parser.add_argument("--appearance_params", help="compare flag", action="store_true")
parser.add_argument("files", nargs="+", help="name of one or more archtype files")
args = parser.parse_args()
print("files",args.files)
file_trees = [etree.parse(filename) for filename in args.files]
print(args)
if args.compare:
compare_trees(file_trees)
if args.appearance_params:
dump_appearance_params(file_trees[0])
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""\
This module contains tools for manipulating collada files
$LicenseInfo:firstyear=2016&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2016, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
import random
# Need to pip install numpy and pycollada
import numpy as np
from collada import *
from lxml import etree
def mesh_summary(mesh):
print("scenes",mesh.scenes)
for scene in mesh.scenes:
print("scene",scene)
for node in scene.nodes:
print("node",node)
def mesh_lock_offsets(tree, joints):
print("mesh_lock_offsets",tree,joints)
for joint_node in tree.iter():
if "node" not in joint_node.tag:
continue
if joint_node.get("type") != "JOINT":
continue
if joint_node.get("name") in joints or "bone" in joints:
for matrix_node in list(joint_node):
if "matrix" in matrix_node.tag:
floats = [float(x) for x in matrix_node.text.split()]
if len(floats) == 16:
floats[3] += 0.0001
floats[7] += 0.0001
floats[11] += 0.0001
matrix_node.text = " ".join([str(f) for f in floats])
print(joint_node.get("name"),matrix_node.tag,"text",matrix_node.text,len(floats),floats)
def mesh_random_offsets(tree, joints):
print("mesh_random_offsets",tree,joints)
for joint_node in tree.iter():
if "node" not in joint_node.tag:
continue
if joint_node.get("type") != "JOINT":
continue
if not joint_node.get("name"):
continue
if joint_node.get("name") in joints or "bone" in joints:
for matrix_node in list(joint_node):
if "matrix" in matrix_node.tag:
floats = [float(x) for x in matrix_node.text.split()]
print("randomizing",floats)
if len(floats) == 16:
floats[3] += random.uniform(-1.0,1.0)
floats[7] += random.uniform(-1.0,1.0)
floats[11] += random.uniform(-1.0,1.0)
matrix_node.text = " ".join([str(f) for f in floats])
print(joint_node.get("name"),matrix_node.tag,"text",matrix_node.text,len(floats),floats)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="process SL animations")
parser.add_argument("--verbose", action="store_true",help="verbose flag")
parser.add_argument("infilename", help="name of a collada (dae) file to input")
parser.add_argument("outfilename", nargs="?", help="name of a collada (dae) file to output", default = None)
parser.add_argument("--lock_offsets", nargs="+", help="tweak position of listed joints to lock their offsets")
parser.add_argument("--random_offsets", nargs="+", help="random offset position for listed joints")
parser.add_argument("--summary", action="store_true", help="print summary info about input file")
args = parser.parse_args()
mesh = None
tree = None
if args.infilename:
print("reading",args.infilename)
mesh = Collada(args.infilename)
tree = etree.parse(args.infilename)
if args.summary:
print("summarizing",args.infilename)
mesh_summary(mesh)
if args.lock_offsets:
print("locking offsets for",args.lock_offsets)
mesh_lock_offsets(tree, args.lock_offsets)
if args.random_offsets:
print("adding random offsets for",args.random_offsets)
mesh_random_offsets(tree, args.random_offsets)
if args.outfilename:
print("writing",args.outfilename)
f = open(args.outfilename,"w")
print(etree.tostring(tree, pretty_print=True), file=f) #need update to get: , short_empty_elements=True)
+520
View File
@@ -0,0 +1,520 @@
#!/usr/bin/env python3
"""\
This module contains tools for manipulating and validating the avatar skeleton file.
$LicenseInfo:firstyear=2016&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2016, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
from lxml import etree
def get_joint_names(tree):
joints = [element.get('name') for element in tree.getroot().iter() if element.tag in ['bone','collision_volume']]
print("joints:",joints)
return joints
def get_aliases(tree):
aliases = {}
alroot = tree.getroot()
for element in alroot.iter():
for key in list(element.keys()):
if key == 'aliases':
name = element.get('name')
val = element.get('aliases')
aliases[name] = val
return aliases
def fix_name(element):
pass
def enforce_precision_rules(element):
pass
def float_tuple(str, n=3):
try:
result = tuple(float(e) for e in str.split())
if len(result)==n:
return result
else:
print("tuple length wrong:", str,"gave",result,"wanted len",n,"got len",len(result))
raise Exception()
except:
print("convert failed for:",str)
raise
def check_symmetry(name, field, vec1, vec2):
if vec1[0] != vec2[0]:
print(name,field,"x match fail")
if vec1[1] != -vec2[1]:
print(name,field,"y mirror image fail")
if vec1[2] != vec2[2]:
print(name,field,"z match fail")
def enforce_alias_rules(tree, element, fix=False):
if element.tag != "bone":
return
alias_lis = []
aliases = element.get("aliases")
if aliases:
alias_lis = aliases.split(" ")
name = element.get("name")
if name:
std_alias = "avatar_" + name
if not std_alias in alias_lis:
print "missing expected alias",name,std_alias
for alias in alias_lis:
if alias.startswith("avatar_") and alias != std_alias:
print "invalid avatar_ alias",name,alias
def enforce_symmetry(tree, element, field, fix=False):
name = element.get("name")
if not name:
return
if "Right" in name:
left_name = name.replace("Right","Left")
left_element = get_element_by_name(tree, left_name)
pos = element.get(field)
left_pos = left_element.get(field)
pos_tuple = float_tuple(pos)
left_pos_tuple = float_tuple(left_pos)
check_symmetry(name,field,pos_tuple,left_pos_tuple)
def get_element_by_name(tree,name):
if tree is None:
return None
matches = [elt for elt in tree.getroot().iter() if elt.get("name")==name]
if len(matches)==1:
return matches[0]
elif len(matches)>1:
print("multiple matches for name",name)
return None
else:
return None
def list_skel_tree(tree):
for element in tree.getroot().iter():
if element.tag == "bone":
print(element.get("name"),"-",element.get("support"))
def validate_child_order(tree, ogtree, fix=False):
unfixable = 0
#print "validate_child_order am failing for NO RAISIN!"
#unfixable += 1
tofix = set()
for element in tree.getroot().iter():
if element.tag != "bone":
continue
og_element = get_element_by_name(ogtree,element.get("name"))
if og_element is not None:
for echild,ochild in zip(list(element),list(og_element)):
if echild.get("name") != ochild.get("name"):
print("Child ordering error, parent",element.get("name"),echild.get("name"),"vs",ochild.get("name"))
if fix:
tofix.add(element.get("name"))
children = {}
for name in tofix:
print("FIX",name)
element = get_element_by_name(tree,name)
og_element = get_element_by_name(ogtree,name)
children = []
# add children matching the original joints first, in the same order
for og_elt in list(og_element):
elt = get_element_by_name(tree,og_elt.get("name"))
if elt is not None:
children.append(elt)
print("b:",elt.get("name"))
else:
print("b missing:",og_elt.get("name"))
# then add children that are not present in the original joints
for elt in list(element):
og_elt = get_element_by_name(ogtree,elt.get("name"))
if og_elt is None:
children.append(elt)
print("e:",elt.get("name"))
# if we've done this right, we have a rearranged list of the same length
if len(children)!=len(element):
print("children",[e.get("name") for e in children])
print("element",[e.get("name") for e in element])
print("children changes for",name,", cannot reconcile")
else:
element[:] = children
return unfixable
# Checklist for the final file, started from SL-276:
# - new "end" attribute on all bones
# - new "connected" attribute on all bones
# - new "support" tag on all bones and CVs
# - aliases where appropriate for backward compatibility. rFoot and lFoot associated with mAnkle bones (not mFoot bones)
# - correct counts of bones and collision volumes in header
# - check all comments
# - old fields of old bones and CVs should be identical to their previous values.
# - old bones and CVs should retain their previous ordering under their parent, with new joints going later in any given child list
# - corresponding right and left joints should be mirror symmetric.
# - childless elements should be in short form (<bone /> instead of <bone></bone>)
# - digits of precision should be consistent (again, except for old joints)
# - new bones should have pos, pivot the same
def validate_skel_tree(tree, ogtree, reftree, fix=False):
print("validate_skel_tree")
(num_bones,num_cvs) = (0,0)
unfixable = 0
defaults = {"connected": "false",
"group": "Face"
}
for element in tree.getroot().iter():
og_element = get_element_by_name(ogtree,element.get("name"))
ref_element = get_element_by_name(reftree,element.get("name"))
# Preserve values from og_file:
for f in ["pos","rot","scale","pivot"]:
if og_element is not None and og_element.get(f) and (str(element.get(f)) != str(og_element.get(f))):
print(element.get("name"),"field",f,"has changed:",og_element.get(f),"!=",element.get(f))
if fix:
element.set(f, og_element.get(f))
# Pick up any other fields that we can from ogtree and reftree
fields = []
if element.tag in ["bone","collision_volume"]:
fields = ["support","group"]
if element.tag == 'bone':
fields.extend(["end","connected"])
for f in fields:
if not element.get(f):
print(element.get("name"),"missing required field",f)
if fix:
if og_element is not None and og_element.get(f):
print("fix from ogtree")
element.set(f,og_element.get(f))
elif ref_element is not None and ref_element.get(f):
print("fix from reftree")
element.set(f,ref_element.get(f))
else:
if f in defaults:
print("fix by using default value",f,"=",defaults[f])
element.set(f,defaults[f])
elif f == "support":
if og_element is not None:
element.set(f,"base")
else:
element.set(f,"extended")
else:
print("unfixable:",element.get("name"),"no value for field",f)
unfixable += 1
fix_name(element)
enforce_alias_rules(tree, element, fix)
enforce_precision_rules(element)
for field in ["pos","pivot"]:
enforce_symmetry(tree, element, field, fix)
if element.get("support")=="extended":
if element.get("pos") != element.get("pivot"):
print("extended joint",element.get("name"),"has mismatched pos, pivot")
if element.tag == "linden_skeleton":
num_bones = int(element.get("num_bones"))
num_cvs = int(element.get("num_collision_volumes"))
all_bones = [e for e in tree.getroot().iter() if e.tag=="bone"]
all_cvs = [e for e in tree.getroot().iter() if e.tag=="collision_volume"]
if num_bones != len(all_bones):
print("wrong bone count, expected",len(all_bones),"got",num_bones)
if fix:
element.set("num_bones", str(len(all_bones)))
if num_cvs != len(all_cvs):
print("wrong cv count, expected",len(all_cvs),"got",num_cvs)
if fix:
element.set("num_collision_volumes", str(len(all_cvs)))
print("skipping child order code")
#unfixable += validate_child_order(tree, ogtree, fix)
if fix and (unfixable > 0):
print("BAD FILE:", unfixable,"errs could not be fixed")
def slider_info(ladtree,skeltree):
for param in ladtree.iter("param"):
for skel_param in param.iter("param_skeleton"):
bones = [b for b in skel_param.iter("bone")]
if bones:
print("param",param.get("name"),"id",param.get("id"))
value_min = float(param.get("value_min"))
value_max = float(param.get("value_max"))
neutral = 100.0 * (0.0-value_min)/(value_max-value_min)
print(" neutral",neutral)
for b in bones:
scale = float_tuple(b.get("scale","0 0 0"))
offset = float_tuple(b.get("offset","0 0 0"))
print(" bone", b.get("name"), "scale", scale, "offset", offset)
scale_min = [value_min * s for s in scale]
scale_max = [value_max * s for s in scale]
offset_min = [value_min * t for t in offset]
offset_max = [value_max * t for t in offset]
if (scale_min != scale_max):
print(" Scale MinX", scale_min[0])
print(" Scale MinY", scale_min[1])
print(" Scale MinZ", scale_min[2])
print(" Scale MaxX", scale_max[0])
print(" Scale MaxY", scale_max[1])
print(" Scale MaxZ", scale_max[2])
if (offset_min != offset_max):
print(" Offset MinX", offset_min[0])
print(" Offset MinY", offset_min[1])
print(" Offset MinZ", offset_min[2])
print(" Offset MaxX", offset_max[0])
print(" Offset MaxY", offset_max[1])
print(" Offset MaxZ", offset_max[2])
# Check contents of avatar_lad file relative to a specified skeleton
def validate_lad_tree(ladtree,skeltree,orig_ladtree):
print("validate_lad_tree")
bone_names = [elt.get("name") for elt in skeltree.iter("bone")]
bone_names.append("mScreen")
bone_names.append("mRoot")
cv_names = [elt.get("name") for elt in skeltree.iter("collision_volume")]
#print "bones\n ","\n ".join(sorted(bone_names))
#print "cvs\n ","\n ".join(sorted(cv_names))
for att in ladtree.iter("attachment_point"):
att_name = att.get("name")
#print "attachment",att_name
joint_name = att.get("joint")
if not joint_name in bone_names:
print("att",att_name,"linked to invalid joint",joint_name)
for skel_param in ladtree.iter("param_skeleton"):
skel_param_id = skel_param.get("id")
skel_param_name = skel_param.get("name")
#if not skel_param_name and not skel_param_id:
# print "strange skel_param"
# print etree.tostring(skel_param)
# for k,v in skel_param.attrib.iteritems():
# print k,"->",v
for bone in skel_param.iter("bone"):
bone_name = bone.get("name")
if not bone_name in bone_names:
print("skel param references invalid bone",bone_name)
print(etree.tostring(bone))
bone_scale = float_tuple(bone.get("scale","0 0 0"))
bone_offset = float_tuple(bone.get("offset","0 0 0"))
param = bone.getparent().getparent()
if bone_scale==(0, 0, 0) and bone_offset==(0, 0, 0):
print("no-op bone",bone_name,"in param",param.get("id","-1"))
# check symmetry of sliders
if "Right" in bone.get("name"):
left_name = bone_name.replace("Right","Left")
left_bone = None
for b in skel_param.iter("bone"):
if b.get("name")==left_name:
left_bone = b
if left_bone is None:
print("left_bone not found",left_name,"in",param.get("id","-1"))
else:
left_scale = float_tuple(left_bone.get("scale","0 0 0"))
left_offset = float_tuple(left_bone.get("offset","0 0 0"))
if left_scale != bone_scale:
print("scale mismatch between",bone_name,"and",left_name,"in param",param.get("id","-1"))
param_id = int(param.get("id","-1"))
if param_id in [661]: # shear
expected_offset = tuple([bone_offset[0],bone_offset[1],-bone_offset[2]])
elif param_id in [30656, 31663, 32663]: # shift
expected_offset = bone_offset
else:
expected_offset = tuple([bone_offset[0],-bone_offset[1],bone_offset[2]])
if left_offset != expected_offset:
print("offset mismatch between",bone_name,"and",left_name,"in param",param.get("id","-1"))
drivers = {}
for driven_param in ladtree.iter("driven"):
driver = driven_param.getparent().getparent()
driven_id = driven_param.get("id")
driver_id = driver.get("id")
actual_param = next(param for param in ladtree.iter("param") if param.get("id")==driven_id)
if not driven_id in drivers:
drivers[driven_id] = set()
drivers[driven_id].add(driver_id)
if (actual_param.get("value_min") != driver.get("value_min") or \
actual_param.get("value_max") != driver.get("value_max")):
if args.verbose:
print("MISMATCH min max:",driver.get("id"),"drives",driven_param.get("id"),"min",driver.get("value_min"),actual_param.get("value_min"),"max",driver.get("value_max"),actual_param.get("value_max"))
for driven_id in drivers:
dset = drivers[driven_id]
if len(dset) != 1:
print("driven_id",driven_id,"has multiple drivers",dset)
else:
if args.verbose:
print("driven_id",driven_id,"has one driver",dset)
if orig_ladtree:
# make sure expected message format is unchanged
orig_message_params_by_id = dict((int(param.get("id")),param) for param in orig_ladtree.iter("param") if param.get("group") in ["0","3"])
orig_message_ids = sorted(orig_message_params_by_id.keys())
#print "orig_message_ids",orig_message_ids
message_params_by_id = dict((int(param.get("id")),param) for param in ladtree.iter("param") if param.get("group") in ["0","3"])
message_ids = sorted(message_params_by_id.keys())
#print "message_ids",message_ids
if (set(message_ids) != set(orig_message_ids)):
print("mismatch in message ids!")
print("added",set(message_ids) - set(orig_message_ids))
print("removed",set(orig_message_ids) - set(message_ids))
else:
print("message ids OK")
def remove_joint_by_name(tree, name):
print("remove joint:",name)
elt = get_element_by_name(tree,name)
while elt is not None:
children = list(elt)
parent = elt.getparent()
print("graft",[e.get("name") for e in children],"into",parent.get("name"))
print("remove",elt.get("name"))
#parent_children = list(parent)
loc = parent.index(elt)
parent[loc:loc+1] = children
elt[:] = []
print("parent now:",[e.get("name") for e in list(parent)])
elt = get_element_by_name(tree,name)
def compare_skel_trees(atree,btree):
diffs = {}
realdiffs = {}
a_missing = set()
b_missing = set()
a_names = set(e.get("name") for e in atree.getroot().iter() if e.get("name"))
b_names = set(e.get("name") for e in btree.getroot().iter() if e.get("name"))
print("a_names\n ",str("\n ").join(sorted(list(a_names))))
print()
print("b_names\n ","\n ".join(sorted(list(b_names))))
all_names = set.union(a_names,b_names)
for name in all_names:
if not name:
continue
a_element = get_element_by_name(atree,name)
b_element = get_element_by_name(btree,name)
if a_element is None or b_element is None:
print("something not found for",name,a_element,b_element)
if a_element is not None and b_element is not None:
all_attrib = set.union(set(a_element.attrib.keys()),set(b_element.attrib.keys()))
print(name,all_attrib)
for att in all_attrib:
if a_element.get(att) != b_element.get(att):
if not att in diffs:
diffs[att] = set()
diffs[att].add(name)
print("tuples",name,att,float_tuple(a_element.get(att)),float_tuple(b_element.get(att)))
if float_tuple(a_element.get(att)) != float_tuple(b_element.get(att)):
print("diff in",name,att)
if not att in realdiffs:
realdiffs[att] = set()
realdiffs[att].add(name)
for att in diffs:
print("Differences in",att)
for name in sorted(diffs[att]):
print(" ",name)
for att in realdiffs:
print("Real differences in",att)
for name in sorted(diffs[att]):
print(" ",name)
a_missing = b_names.difference(a_names)
b_missing = a_names.difference(b_names)
if len(a_missing) or len(b_missing):
print("Missing from comparison")
for name in a_missing:
print(" ",name)
print("Missing from infile")
for name in b_missing:
print(" ",name)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="process SL avatar_skeleton/avatar_lad files")
parser.add_argument("--verbose", action="store_true",help="verbose flag")
parser.add_argument("--ogfile", help="specify file containing base bones", default="avatar_skeleton_orig.xml")
parser.add_argument("--ref_file", help="specify another file containing replacements for missing fields")
parser.add_argument("--lad_file", help="specify avatar_lad file to check", default="avatar_lad.xml")
parser.add_argument("--orig_lad_file", help="specify avatar_lad file to compare to", default="avatar_lad_orig.xml")
parser.add_argument("--aliases", help="specify file containing bone aliases")
parser.add_argument("--validate", action="store_true", help="check specified input file for validity")
parser.add_argument("--fix", action="store_true", help="try to correct errors")
parser.add_argument("--remove", nargs="+", help="remove specified joints")
parser.add_argument("--list", action="store_true", help="list joint names")
parser.add_argument("--compare", help="alternate skeleton file to compare")
parser.add_argument("--slider_info", help="information about the lad file sliders and affected bones", action="store_true")
parser.add_argument("infilename", nargs="?", help="name of a skel .xml file to input", default="avatar_skeleton.xml")
parser.add_argument("outfilename", nargs="?", help="name of a skel .xml file to output")
args = parser.parse_args()
tree = etree.parse(args.infilename)
aliases = {}
if args.aliases:
altree = etree.parse(args.aliases)
aliases = get_aliases(altree)
# Parse input files
ogtree = None
reftree = None
ladtree = None
orig_ladtree = None
if args.ogfile:
ogtree = etree.parse(args.ogfile)
if args.ref_file:
reftree = etree.parse(args.ref_file)
if args.lad_file:
ladtree = etree.parse(args.lad_file)
if args.orig_lad_file:
orig_ladtree = etree.parse(args.orig_lad_file)
if args.remove:
for name in args.remove:
remove_joint_by_name(tree,name)
# Do processing
if args.validate and ogtree:
validate_skel_tree(tree, ogtree, reftree)
if args.validate and ladtree:
validate_lad_tree(ladtree, tree, orig_ladtree)
if args.fix and ogtree:
validate_skel_tree(tree, ogtree, reftree, True)
if args.list and tree:
list_skel_tree(tree)
if args.compare and tree:
compare_tree = etree.parse(args.compare)
compare_skel_trees(compare_tree,tree)
if ladtree and tree and args.slider_info:
slider_info(ladtree,tree)
if args.outfilename:
f = open(args.outfilename,"w")
print(etree.tostring(tree, pretty_print=True), file=f) #need update to get: , short_empty_elements=True)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""\
@file md5check.py
@brief Replacement for message template compatibility verifier.
$LicenseInfo:firstyear=2010&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2010-2011, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import sys
import hashlib
if len(sys.argv) != 3:
print("""Usage: %s --create|<hash-digest> <file>
Creates an md5sum hash digest of the specified file content
and compares it with the given hash digest.
If --create is used instead of a hash digest, it will simply
print out the hash digest of specified file content.
""" % sys.argv[0])
sys.exit(1)
if sys.argv[2] == '-':
fh = sys.stdin
filename = "<stdin>"
else:
filename = sys.argv[2]
fh = open(filename)
hexdigest = hashlib.md5(fh.read()).hexdigest()
if sys.argv[1] == '--create':
print(hexdigest)
elif hexdigest == sys.argv[1]:
print("md5sum check passed:", filename)
else:
print("md5sum check FAILED:", filename)
sys.exit(1)
+9219
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
1a9a3717fde5d0fb3d5f688a1a3dab7fcc2aa308
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/python
"""\
@file slp_conv.py
@author Callum Prentice
@date 2021-01-26
@brief Convert a Second Life Performance (SLP) file generated
by the Viewer into an comma separated value (CSV) file
for import into spreadsheets and other data analytics tools.
$LicenseInfo:firstyear=2021&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2021, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import llsd
import argparse
parser = argparse.ArgumentParser(
description="Converts Viewer SLP files into CSV for import into spreadsheets etc."
)
parser.add_argument(
"infilename",
help="Name of SLP file to read",
)
parser.add_argument(
"outfilename",
help="Name of CSV file to create",
)
args = parser.parse_args()
with open(args.infilename, "r") as slp_file:
slps = slp_file.readlines()
print "Reading from %s - %d items" % (args.infilename, len(slps))
with open(args.outfilename, "w") as csv_file:
print "Writing to %s" % args.outfilename
for index, each_slp in enumerate(slps):
slp_entry = llsd.parse(each_slp)
first_key = slp_entry.keys()[0]
# first entry so write column headers
if index == 0:
line = ""
for key, value in slp_entry[first_key].iteritems():
line += key
line += ", "
csv_file.write("entry, %s, \n" % line)
# write line of data
line = ""
for key, value in slp_entry[first_key].iteritems():
line += str(value)
line += ", "
csv_file.write("%s, %s, \n" % (first_key, str(line)))
+102
View File
@@ -0,0 +1,102 @@
#!runpy.sh
"""\
This module contains tools for analyzing viewer asset metrics logs produced by the viewer.
$LicenseInfo:firstyear=2016&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2016, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
from lxml import etree
import llsd
def get_metrics_record(infiles):
for filename in args.infiles:
f = open(filename)
# get an iterable
context = etree.iterparse(f, events=("start", "end"))
# turn it into an iterator
context = iter(context)
# get the root element
event, root = next(context)
try:
for event, elem in context:
if event == "end" and elem.tag == "llsd":
xmlstr = etree.tostring(elem, encoding="utf8", method="xml")
sd = llsd.parse_xml(xmlstr)
yield sd
except etree.XMLSyntaxError:
print("Fell off end of document")
f.close()
def update_stats(stats,rec):
for region in rec["regions"]:
region_key = (region["grid_x"],region["grid_y"])
#print "region",region_key
for field, val in region.items():
if field in ["duration","grid_x","grid_y"]:
continue
if field == "fps":
# handle fps record as special case
pass
else:
#print "field",field
stats.setdefault(field,{})
type_stats = stats.get(field)
newcount = val["resp_count"]
#print "field",field,"add count",newcount
type_stats["count"] = type_stats.get("count",0) + val["resp_count"]
#print "field",field,"count",type_stats["count"]
if (newcount>0):
type_stats["sum"] = type_stats.get("sum",0) + val["resp_count"] * val["resp_mean"]
type_stats["sum_bytes"] = type_stats.get("sum_bytes",0) + val["resp_count"] * val.get("resp_mean_bytes",0)
type_stats["enqueued"] = type_stats.get("enqueued",0) + val["enqueued"]
type_stats["dequeued"] = type_stats.get("dequeued",0) + val["dequeued"]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="process metric xml files for viewer asset fetching")
parser.add_argument("--verbose", action="store_true",help="verbose flag")
parser.add_argument("infiles", nargs="+", help="name of .xml files to process")
args = parser.parse_args()
#print "process files:",args.infiles
stats = {}
for rec in get_metrics_record(args.infiles):
#print "record",rec
update_stats(stats,rec)
for key in sorted(stats.keys()):
val = stats[key]
if val["count"] > 0:
print(key,"count",val["count"],"mean_time",val["sum"]/val["count"],"mean_bytes",val["sum_bytes"]/val["count"],"net bytes/sec",val["sum_bytes"]/val["sum"],"enqueued",val["enqueued"],"dequeued",val["dequeued"])
else:
print(key,"count",val["count"],"enqueued",val["enqueued"],"dequeued",val["dequeued"])
+226
View File
@@ -0,0 +1,226 @@
#!runpy.sh
"""\
This module contains code for analyzing ViewerStats data as uploaded by the viewer.
$LicenseInfo:firstyear=2021&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2021, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
import numpy as np
import pandas as pd
import json
from collections import Counter, defaultdict
import llsd
import io
import re
import os
import sys
def show_stats_by_key(recs,indices,settings_sd = None):
result = ()
cnt = Counter()
per_key_cnt = defaultdict(Counter)
for r in recs:
try:
d = r
for idx in indices:
d = d[idx]
for k,v in d.items():
if isinstance(v,dict):
continue
cnt[k] += 1
if isinstance(v,list):
v = tuple(v)
per_key_cnt[k][v] += 1
except Exception as e:
print("err", e)
print("d", d, "k", k, "v", v)
raise
mc = cnt.most_common()
print("=========================")
keyprefix = ""
if len(indices)>0:
keyprefix = ".".join(indices) + "."
for i,m in enumerate(mc):
k = m[0]
bigc = m[1]
unset_cnt = len(recs) - bigc
kmc = per_key_cnt[k].most_common(5)
print(i, keyprefix+str(k), bigc)
if settings_sd is not None and k in settings_sd and "Value" in settings_sd[k]:
print(" ", "default",settings_sd[k]["Value"],"count",unset_cnt)
for v in kmc:
print(" ", "value",v[0],"count",v[1])
if settings_sd is not None:
print("Total keys in settings", len(settings_sd.keys()))
unused_keys = list(set(settings_sd.keys()) - set(cnt.keys()))
unused_keys_non_str = [k for k in unused_keys if settings_sd[k]["Type"] != "String"]
unused_keys_str = [k for k in unused_keys if settings_sd[k]["Type"] == "String"]
# Things that no one in the sample has set to a non-default value. Possible candidates for removal.
print("\nUnused_keys_non_str", len(unused_keys_non_str))
print( "======================")
print("\n".join(sorted(unused_keys_non_str)))
# Strings are not currently logged, so we have no info on usage.
print("\nString keys (usage unknown)", len(unused_keys_str))
print( "======================")
print("\n".join(sorted(unused_keys_str)))
# Things that someone has set but that aren't recognized settings.
unrec_keys = list(set(cnt.keys()) - set(settings_sd.keys()))
print("\nUnrecognized keys", len(unrec_keys))
print( "======================")
print("\n".join(sorted(unrec_keys)))
result = (settings_sd.keys(), unused_keys_str, unused_keys_non_str, unrec_keys)
return result
def parse_settings_xml(fname):
# assume we're in scripts/metrics
fname = "../../indra/newview/app_settings/" + fname
with open(fname,"r") as f:
contents = f.read()
return llsd.parse_xml(contents)
def read_raw_settings_xml(fname):
# assume we're in scripts/metrics
fname = "../../indra/newview/app_settings/" + fname
contents = None
with open(fname,"r") as f:
contents = f.read()
return contents
def write_settings_xml(fname, contents):
# assume we're in scripts/metrics
fname = "../../indra/newview/app_settings/" + fname
with open(fname,"w") as f:
f.write(llsd.format_pretty_xml(contents))
f.close()
def write_raw_settings_xml(fname, string):
# assume we're in scripts/metrics
fname = "../../indra/newview/app_settings/" + fname
with io.open(fname,"w", newline='\n') as f:
f.write(string.decode('latin1'))
f.close()
def remove_settings(string, to_remove):
for r in to_remove:
subs_str = r"<key>" + r + r"<.*?</map>\n"
string = re.sub(subs_str,"",string,flags=re.S|re.DOTALL)
return string
def get_used_strings(root_dir):
used_str = set()
skipped_ext = set()
for dir_name, sub_dir_list, file_list in os.walk(root_dir):
for fname in file_list:
if fname in ["settings.xml", "settings.xml.edit", "settings_per_account.xml"]:
print("skip", fname)
continue
(base,ext) = os.path.splitext(fname)
#if ext not in [".cpp", ".hpp", ".h", ".xml"]:
# skipped_ext.add(ext)
# continue
full_name = os.path.join(dir_name,fname)
with open(full_name,"r") as f:
#print full_name
lines = f.readlines()
for l in lines:
ms = re.findall(r'[>\"]([A-Za-z0-9_]+)[\"<]',l)
for m in ms:
#print "used_str",m
used_str.add(m)
print("skipped extensions", skipped_ext)
print("got used_str", len(used_str))
return used_str
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="process tab-separated table containing viewerstats logs")
parser.add_argument("--verbose", action="store_true",help="verbose flag")
parser.add_argument("--preferences", action="store_true", help="analyze preference info")
parser.add_argument("--remove_unused", action="store_true", help="remove unused preferences")
parser.add_argument("--column", help="name of column containing viewerstats info")
parser.add_argument("infiles", nargs="+", help="name of .tsv files to process")
args = parser.parse_args()
for fname in args.infiles:
print("process", fname)
df = pd.read_csv(fname,sep='\t')
#print "DF", df.describe()
jstrs = df['RAW_LOG:BODY']
#print "JSTRS", jstrs.describe()
recs = []
for i,jstr in enumerate(jstrs):
recs.append(json.loads(jstr))
show_stats_by_key(recs,[])
show_stats_by_key(recs,["agent"])
if args.preferences:
print("\nSETTINGS.XML")
settings_sd = parse_settings_xml("settings.xml")
#for skey,svals in settings_sd.items():
# print skey, "=>", svals
(all_str,_,_,_) = show_stats_by_key(recs,["preferences","settings"],settings_sd)
print()
#print "\nSETTINGS_PER_ACCOUNT.XML"
#settings_pa_sd = parse_settings_xml("settings_per_account.xml")
#show_stats_by_key(recs,["preferences","settings_per_account"],settings_pa_sd)
if args.remove_unused:
# walk codebase looking for strings
all_str_set = set(all_str)
used_strings = get_used_strings("../../indra")
used_strings_set = set(used_strings)
unref_strings = all_str_set-used_strings_set
# Some settings names are generated by appending to a prefix. Need to look for this case.
prefix_used = set()
print("checking unref_strings", len(unref_strings))
for u in unref_strings:
for k in range(6,len(u)):
prefix = u[0:k]
if prefix in all_str_set and prefix in used_strings_set:
prefix_used.add(u)
#print "PREFIX_USED",u,prefix
print("PREFIX_USED", len(prefix_used), ",".join(list(prefix_used)))
print()
unref_strings = unref_strings - prefix_used
print("\nUNREF_IN_CODE " + str(len(unref_strings)) + "\n")
print("\n".join(list(unref_strings)))
settings_str = read_raw_settings_xml("settings.xml")
# Do this via direct string munging to generate minimal changeset
settings_edited = remove_settings(settings_str,unref_strings)
write_raw_settings_xml("settings.xml.edit",settings_edited)
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""\
This module formats the package version and copyright information for the
viewer and its dependent packages.
$LicenseInfo:firstyear=2014&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2014, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import os
import sys
import errno
import re
import subprocess
import argparse
parser = argparse.ArgumentParser(description='Format dependency version and copyright information for the viewer About box content')
parser.add_argument('channel', help='viewer channel name')
parser.add_argument('version', help='viewer version number')
parser.add_argument('install_dir', help="install dir of packages")
args = parser.parse_args()
_autobuild=os.getenv('AUTOBUILD', 'autobuild')
_autobuild_env=os.environ.copy()
# Coerce stdout encoding to utf-8 as cygwin's will be detected as cp1252 otherwise.
_autobuild_env["PYTHONIOENCODING"] = "utf-8"
pkg_line=re.compile('^([\w-]+):\s+(.*)$')
def autobuild(*args):
"""
Launch autobuild with specified command-line arguments.
Return its stdout pipe from which the caller can read.
"""
# subprocess wants a list, not a tuple
command = [_autobuild] + list(args)
try:
child = subprocess.Popen(command,
stdin=None, stdout=subprocess.PIPE,
universal_newlines=True, env=_autobuild_env)
except OSError as err:
if err.errno != errno.ENOENT:
# Don't attempt to interpret anything but ENOENT
raise
# Here it's ENOENT: subprocess can't find the autobuild executable.
sys.exit("packages-formatter on %s: can't run autobuild:\n%s\n%s" % \
(sys.platform, ' '.join(command), err))
# no exceptions yet, let caller read stdout
return child.stdout
info=dict(versions={}, copyrights={})
dups=dict(versions=set(), copyrights=set())
def add_info(key, pkg, lines):
if pkg not in info[key]:
info[key][pkg] = '\n'.join(lines)
# <FS:Ansariel> Only add as duplicate of the version is duplicate and the copyright string does not match
#else:
elif info[key][pkg] != '\n'.join(lines):
print("key: %s - pkg: %s - line: %s" % (key, pkg, lines))
# </FS:Ansariel>
dups[key].add(pkg)
versions=autobuild('install', '--versions', '--install-dir', args.install_dir)
copyrights=autobuild('install', '--copyrights', '--install-dir', args.install_dir)
viewer_copyright = copyrights.readline() # first line is the copyright for the viewer itself
# Two different autobuild outputs, but we treat them essentially the same way:
# populating each into a dict; each a subdict of 'info'.
for key, rawdata in ("versions", versions), ("copyrights", copyrights):
lines = iter(rawdata)
try:
line = next(lines)
except StopIteration:
# rawdata is completely empty? okay...
pass
else:
pkg_info = pkg_line.match(line)
# The first line for each package must match pkg_line.
if not pkg_info:
sys.exit("Unrecognized --%s output: %r" % (key, line))
# Only the very first line in rawdata MUST match; for the rest of
# rawdata, matching the regexp is how we recognize the start of the
# next package.
while True: # iterate over packages in rawdata
pkg = pkg_info.group(1)
pkg_lines = [pkg_info.group(2).strip()]
for line in lines:
pkg_info = pkg_line.match(line)
if pkg_info:
# we hit the start of the next package data
add_info(key, pkg, pkg_lines)
break
else:
# no package prefix: additional line for same package
pkg_lines.append(line.rstrip())
else:
# last package in the output -- finished 'lines'
add_info(key, pkg, pkg_lines)
break
# Now that we've run through all of both outputs -- are there duplicates?
if any(pkgs for pkgs in list(dups.values())):
for key, pkgs in list(dups.items()):
if pkgs:
print("Duplicate %s for %s" % (key, ", ".join(pkgs)), file=sys.stderr)
sys.exit(1)
print("%s %s" % (args.channel, args.version))
print(viewer_copyright)
version = list(info['versions'].items())
version.sort()
for pkg, pkg_version in version:
print(': '.join([pkg, pkg_version]))
try:
print(info['copyrights'][pkg])
except KeyError:
sys.exit("No copyright for %s" % pkg)
print()
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""\
@file logsdir.py
@author Nat Goodspeed
@date 2024-09-12
@brief Locate the Second Life logs directory for the current user on the
current platform.
$LicenseInfo:firstyear=2024&license=viewerlgpl$
Copyright (c) 2024, Linden Research, Inc.
$/LicenseInfo$
"""
import os
from pathlib import Path
import platform
class Error(Exception):
pass
# logic used by SLVersionChecker
def logsdir():
app = 'SecondLife'
system = platform.system()
if (system == 'Darwin'):
base_dir = os.path.join(os.path.expanduser('~'),
'Library','Application Support',app)
elif (system == 'Linux'):
base_dir = os.path.join(os.path.expanduser('~'),
'.' + app.lower())
elif (system == 'Windows'):
appdata = os.getenv('APPDATA')
base_dir = os.path.join(appdata, app)
else:
raise ValueError("Unsupported platform '%s'" % system)
return os.path.join(base_dir, 'logs')
def latest_file(dirpath, pattern):
files = Path(dirpath).glob(pattern)
sort = [(p.stat().st_mtime, p) for p in files if p.is_file()]
sort.sort(reverse=True)
try:
return sort[0][1]
except IndexError:
raise Error(f'No {pattern} files in {dirpath}')
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""\
@file perfbot_run.py
@brief Run a number of non interactive Viewers (PerfBots) with
a variety of options and settings. Pass --help for details.
$LicenseInfo:firstyear=2007&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2021, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import argparse
import subprocess
import os
import math
import time
# Required parameters that are always passed in
# Specify noninteractive mode (SL-15999 for details)
PARAM_NON_INTERACTIVE = "--noninteractive"
# Run multiple Viewers at once
PARAM_MULTI = "--multiple"
# Specify username (first and last) and password
PARAM_LOGIN = "--login"
# SLURL to teleport to after login
PARAM_SLURL = "--slurl"
def gen_niv_script(args):
print(f"Reading creds from {(args.creds)} folder")
print(f"Using the non interactive Viewer from {(args.viewer)}")
print(f"Sleeping for {args.sleep}ms between Viewer launches")
# Read the lines from the creds file. Typically this will be
# stored in the build-secrets-git private repository but you
# can point to any location with the --creds parameter
creds_lines = []
with open(args.creds) as file:
creds_lines = file.readlines()
creds_lines = [line.rstrip() for line in creds_lines]
creds_lines = [line for line in creds_lines if not line.startswith("#") and len(line)]
# We cannot log in more users than we have credentials for
if args.num==0:
args.num = len(creds_lines)
if args.num > len(creds_lines):
print(
f"The number of agents specified ({(args.num)}) exceeds "
f"the number of valid entries ({(len(creds_lines))}) in "
f"the creds file "
)
return
print(f"Launching {(args.num)} instances of the Viewer")
# The Viewer (in dev environments at least) needs a well specified
# working directory to function properly. We try to guess what it
# might be based on the full path to the Viewer executable but
# you can also specify it explicitly with the --cwd parameter
# (required for dev builds)
args.viewer = os.path.abspath(args.viewer)
if len(args.cwd) == 0:
working_dir = os.path.dirname(os.path.abspath(args.viewer))
else:
working_dir = os.path.abspath(args.cwd)
print(f"Working directory is {working_dir}, cwd {args.cwd}")
os.chdir(working_dir)
if args.dryrun:
print("Running in dry-run mode - no Viewers will be started")
print("")
for inst in range(args.num):
# Format of each cred line is username_first username_last password
# A space is used to separate each and a # at the start of a line
# removes it from the pool (useful if someone else is using a subset
# of the available ones)
creds = creds_lines[inst].split(" ")
username_first = creds[0]
username_last = creds[1]
password = creds[2]
# The default layout is an evenly spaced circle in the
# center of the region. We may extend this to allow other
# patterns like a square/rectangle or a spiral. (Hint: it
# likely won't be needed :))
center_x = 128
center_y = 128
if args.layout == "circle":
radius = 6
angle = (2 * math.pi / args.num) * inst
region_x = int(math.sin(angle) * radius + center_x)
region_y = int(math.cos(angle) * radius + center_y)
region_z = 0
elif args.layout == "square":
region_x = center_x
region_y = center_y
elif args.layout == "spiral":
region_x = center_x
region_y = center_y
slurl = f"secondlife://{args.region}/{region_x}/{region_y}/{region_z}"
# Build the script line
script_cmd = [args.viewer]
script_cmd.append(PARAM_NON_INTERACTIVE)
script_cmd.append(PARAM_MULTI)
script_cmd.append(PARAM_LOGIN)
script_cmd.append(username_first)
script_cmd.append(username_last)
script_cmd.append(password)
script_cmd.append(PARAM_SLURL)
script_cmd.append(slurl)
# Display the script we will execute.
cmd = ""
for p in script_cmd:
cmd = cmd + " " + p
print(cmd)
# If --dry-run is specified, we do everything (including, most
# usefully, display the script lines) but do not start the Viewer
if args.dryrun == False:
print("opening viewer session with",script_cmd)
viewer_session = subprocess.Popen(script_cmd)
# Sleeping a bit between launches seems to help avoid a CPU
# surge when N Viewers are started simulatanously. The default
# value can be changed with the --sleep parameter
time.sleep(args.sleep / 1000)
if __name__ == "__main__":
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument(
"--num",
type=int,
default=0,
dest="num",
help="How many avatars to add to the script",
)
parser.add_argument(
"--creds",
default="../../../build-secrets-git/perf/perfbot_creds.txt",
dest="creds",
help="Location of the text file containing user credentials",
)
parser.add_argument(
"--viewer",
default="C:/Program Files/SecondLife/SecondLifeViewer.exe",
dest="viewer",
help="Location of the non interactive Viewer build",
)
parser.add_argument(
"--cwd",
default="",
dest="cwd",
help="Location of the current working directory to use",
)
parser.add_argument(
"--region",
default="Lag Me 5",
dest="region",
help="The SLURL for the Second Life region to visit",
)
parser.add_argument(
"--layout",
default="circle",
dest="layout",
choices={"circle", "square", "spiral"},
help="The geometric layout of the avatar destination locations",
)
parser.add_argument(
"--sleep",
type=int,
default=1000,
dest="sleep",
help="Time to sleep between launches in milliseconds",
)
parser.add_argument(
"--dry-run",
action="store_true",
dest="dryrun",
help="Dryrun mode - display parameters and script lines but do not start any Viewers",
)
args = parser.parse_args()
gen_niv_script(args)
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""\
@file profile_cmp.py
@author Nat Goodspeed
@date 2024-09-13
@brief Compare a frame profile stats file with a similar baseline file.
$LicenseInfo:firstyear=2024&license=viewerlgpl$
Copyright (c) 2024, Linden Research, Inc.
$/LicenseInfo$
"""
from datetime import datetime
import json
from logsdir import Error, latest_file, logsdir
from pathlib import Path
import sys
# variance that's ignorable
DEFAULT_EPSILON = 0.03 # 3%
def compare(baseline, test, epsilon=DEFAULT_EPSILON):
if Path(baseline).samefile(test):
print(f'{baseline} same as\n{test}\nAnalysis moot.')
return
with open(baseline) as inf:
bdata = json.load(inf)
with open(test) as inf:
tdata = json.load(inf)
print(f'baseline {baseline}\ntestfile {test}')
for k, tv in tdata['context'].items():
bv = bdata['context'].get(k)
if bv != tv:
print(f'baseline {k}={bv} vs.\ntestfile {k}={tv}')
btime = bdata['context'].get('time')
ttime = tdata['context'].get('time')
if btime and ttime:
print('testfile newer by',
datetime.fromisoformat(ttime) - datetime.fromisoformat(btime))
# The following ignores totals and unused shaders, except to the extent
# that some shaders were used in the baseline but not in the recent test
# or vice-versa. While the viewer considers that a shader has been used if
# 'binds' is nonzero, we exclude any whose 'time' is zero to avoid zero
# division.
bshaders = {s['name']: s for s in bdata['shaders'] if s['time'] and s['samples']}
tshaders = {s['name']: s for s in tdata['shaders'] if s['time']}
bothshaders = set(bshaders).intersection(tshaders)
deltas = []
for shader in bothshaders:
bshader = bshaders[shader]
tshader = tshaders[shader]
bthruput = bshader['samples']/bshader['time']
tthruput = tshader['samples']/tshader['time']
delta = (tthruput - bthruput)/bthruput
if abs(delta) > epsilon:
deltas.append((delta, shader, bthruput, tthruput))
# descending order of performance gain
deltas.sort(reverse=True)
print(f'{len(deltas)} shaders showed nontrivial performance differences '
'(millon samples/sec):')
namelen = max(len(s[1]) for s in deltas) if deltas else 0
for delta, shader, bthruput, tthruput in deltas:
print(f' {shader.rjust(namelen)} {delta*100:6.1f}% '
f'{bthruput/1000000:8.2f} -> {tthruput/1000000:8.2f}')
tunused = set(bshaders).difference(tshaders)
print(f'{len(tunused)} baseline shaders not used in test:')
for s in tunused:
print(f' {s}')
bunused = set(tshaders).difference(bshaders)
print(f'{len(bunused)} shaders newly used in test:')
for s in bunused:
print(f' {s}')
def main(*raw_args):
from argparse import ArgumentParser
parser = ArgumentParser(description="""
%(prog)s compares a baseline JSON file from Develop -> Render Tests -> Frame
Profile to another such file from a more recent test. It identifies shaders
that have gained and lost in throughput.
""")
parser.add_argument('-e', '--epsilon', type=float, default=int(DEFAULT_EPSILON*100),
help="""percent variance considered ignorable (default %(default)s%%)""")
parser.add_argument('baseline',
help="""baseline profile filename to compare against""")
parser.add_argument('test', nargs='?',
help="""test profile filename to compare
(default is most recent)""")
args = parser.parse_args(raw_args)
compare(args.baseline,
args.test or latest_file(logsdir(), 'profile.*.json'),
epsilon=(args.epsilon / 100.))
if __name__ == "__main__":
try:
sys.exit(main(*sys.argv[1:]))
except (Error, OSError, json.JSONDecodeError) as err:
sys.exit(str(err))
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""\
@file profile_csv.py
@author Nat Goodspeed
@date 2024-09-12
@brief Convert a JSON file from Develop -> Render Tests -> Frame Profile to CSV
$LicenseInfo:firstyear=2024&license=viewerlgpl$
Copyright (c) 2024, Linden Research, Inc.
$/LicenseInfo$
"""
import json
from logsdir import Error, latest_file, logsdir
import sys
def convert(path, totals=True, unused=True, file=sys.stdout):
with open(path) as inf:
data = json.load(inf)
# print path to sys.stderr in case user is redirecting stdout
print(path, file=sys.stderr)
print('"name", "file1", "file2", "time", "binds", "samples", "triangles"', file=file)
if totals:
t = data['totals']
print(f'"totals", "", "", {t["time"]}, {t["binds"]}, {t["samples"]}, {t["triangles"]}',
file=file)
for sh in data['shaders']:
print(f'"{sh["name"]}", "{sh["files"][0]}", "{sh["files"][1]}", '
f'{sh["time"]}, {sh["binds"]}, {sh["samples"]}, {sh["triangles"]}', file=file)
if unused:
for u in data['unused']:
print(f'"{u}", "", "", 0, 0, 0, 0', file=file)
def main(*raw_args):
from argparse import ArgumentParser
parser = ArgumentParser(description="""
%(prog)s converts a JSON file from Develop -> Render Tests -> Frame Profile to
a more-or-less equivalent CSV file. It expands the totals stats and unused
shaders list to full shaders lines.
""")
parser.add_argument('-t', '--totals', action='store_false', default=True,
help="""omit totals from CSV file""")
parser.add_argument('-u', '--unused', action='store_false', default=True,
help="""omit unused shaders from CSV file""")
parser.add_argument('path', nargs='?',
help="""profile filename to convert (default is most recent)""")
args = parser.parse_args(raw_args)
convert(args.path or latest_file(logsdir(), 'profile.*.json'),
totals=args.totals, unused=args.unused)
if __name__ == "__main__":
try:
sys.exit(main(*sys.argv[1:]))
except (Error, OSError, json.JSONDecodeError) as err:
sys.exit(str(err))
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""\
@file profile_pretty.py
@author Nat Goodspeed
@date 2024-09-12
@brief Pretty-print a JSON file from Develop -> Render Tests -> Frame Profile
$LicenseInfo:firstyear=2024&license=viewerlgpl$
Copyright (c) 2024, Linden Research, Inc.
$/LicenseInfo$
"""
import json
from logsdir import Error, latest_file, logsdir
import sys
def pretty(path):
with open(path) as inf:
data = json.load(inf)
# print path to sys.stderr in case user is redirecting stdout
print(path, file=sys.stderr)
json.dump(data, sys.stdout, indent=4)
def main(*raw_args):
from argparse import ArgumentParser
parser = ArgumentParser(description="""
%(prog)s pretty-prints a JSON file from Develop -> Render Tests -> Frame Profile.
The file produced by the viewer is a single dense line of JSON.
""")
parser.add_argument('path', nargs='?',
help="""profile filename to pretty-print (default is most recent)""")
args = parser.parse_args(raw_args)
pretty(args.path or latest_file(logsdir(), 'profile.*.json'))
if __name__ == "__main__":
try:
sys.exit(main(*sys.argv[1:]))
except (Error, OSError, json.JSONDecodeError) as err:
sys.exit(str(err))
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""\
@file setup-path.py
@brief Get the python library directory in the path, so we don't have
to screw with PYTHONPATH or symbolic links.
$LicenseInfo:firstyear=2007&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2010, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import sys
from os.path import realpath, dirname, join
# Walk back to checkout base directory
dir = dirname(dirname(realpath(__file__)))
# Walk in to libraries directory
dir = join(dir, 'indra', 'lib', 'python')
if dir not in sys.path:
sys.path.insert(0, dir)
+334
View File
@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""\
@file template_verifier.py
@brief Message template compatibility verifier.
$LicenseInfo:firstyear=2007&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2010, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
"""template_verifier is a script which will compare the
current repository message template with the "master" message template, accessible
via http://secondlife.com/app/message_template/master_message_template.msg
If [FILE] is specified, it will be checked against the master template.
If [FILE] [FILE] is specified, two local files will be checked against
each other.
"""
import sys
import os.path
# Look for indra/lib/python in all possible parent directories ...
# This is an improvement over the setup-path.py method used previously:
# * the script may blocated anywhere inside the source tree
# * it doesn't depend on the current directory
# * it doesn't depend on another file being present.
def add_indra_lib_path():
root = os.path.realpath(__file__)
# always insert the directory of the script in the search path
dir = os.path.dirname(root)
if dir not in sys.path:
sys.path.insert(0, dir)
# Now go look for indra/lib/python in the parent dies
while root != os.path.sep:
root = os.path.dirname(root)
dir = os.path.join(root, 'indra', 'lib', 'python')
if os.path.isdir(dir):
if dir not in sys.path:
sys.path.insert(0, dir)
break
else:
print("This script is not inside a valid installation.", file=sys.stderr)
sys.exit(1)
add_indra_lib_path()
import optparse
import os
import urllib.request, urllib.parse, urllib.error
import hashlib
from indra.ipc import compatibility
from indra.ipc import tokenstream
from indra.ipc import llmessage
def getstatusall(command):
""" Like commands.getstatusoutput, but returns stdout and
stderr separately(to get around "killed by signal 15" getting
included as part of the file). Also, works on Windows."""
(input, out, err) = os.popen3(command, 't')
status = input.close() # send no input to the command
output = out.read()
error = err.read()
status = out.close()
status = err.close() # the status comes from the *last* pipe that is closed
return status, output, error
def getstatusoutput(command):
status, output, error = getstatusall(command)
return status, output
def die(msg):
print(msg, file=sys.stderr)
sys.exit(1)
MESSAGE_TEMPLATE = 'message_template.msg'
PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer)
DEVELOPMENT_ACCEPTABLE = (
compatibility.Same, compatibility.Newer,
compatibility.Older, compatibility.Mixed)
MAX_MASTER_AGE = 60 * 60 * 4 # refresh master cache every 4 hours
def retry(times, function, *args, **kwargs):
for i in range(times):
try:
return function(*args, **kwargs)
except Exception as e:
if i == times - 1:
raise e # we retried all the times we could
def compare(base_parsed, current_parsed, mode):
"""Compare the current template against the base template using the given
'mode' strictness:
development: Allows Same, Newer, Older, and Mixed
production: Allows only Same or Newer
Print out information about whether the current template is compatible
with the base template.
Returns a tuple of (bool, Compatibility)
Return True if they are compatible in this mode, False if not.
"""
compat = current_parsed.compatibleWithBase(base_parsed)
if mode == 'production':
acceptable = PRODUCTION_ACCEPTABLE
else:
acceptable = DEVELOPMENT_ACCEPTABLE
if type(compat) in acceptable:
return True, compat
return False, compat
def fetch(url):
if url.startswith('file://'):
# just open the file directly because urllib is dumb about these things
file_name = url[len('file://'):]
with open(file_name, 'rb') as f:
return f.read()
else:
with urllib.request.urlopen(url) as res:
body = res.read()
if res.status > 299:
sys.exit("ERROR: Unable to download %s. HTTP status %d.\n%s" % (url, res.status, body.decode("utf-8")))
return body
def cache_master(master_url):
"""Using the url for the master, updates the local cache, and returns an url to the local cache."""
master_cache = local_master_cache_filename()
master_cache_url = 'file://' + master_cache
# decide whether to refresh the master cache based on its age
import time
if (os.path.exists(master_cache)
and time.time() - os.path.getmtime(master_cache) < MAX_MASTER_AGE):
return master_cache_url # our cache is fresh
# new master doesn't exist or isn't fresh
print("Refreshing master cache from %s" % master_url)
def get_and_test_master():
new_master_contents = fetch(master_url)
llmessage.parseTemplateString(new_master_contents.decode("utf-8"))
return new_master_contents
try:
new_master_contents = retry(3, get_and_test_master)
except IOError as e:
# the refresh failed, so we should just soldier on
print("WARNING: unable to download new master, probably due to network error. Your message template compatibility may be suspect.")
print("Cause: %s" % e)
return master_cache_url
try:
tmpname = '%s.%d' % (master_cache, os.getpid())
with open(tmpname, "wb") as mc:
mc.write(new_master_contents)
try:
os.rename(tmpname, master_cache)
except OSError:
# We can't rename atomically on top of an existing file on
# Windows. Unlinking the existing file will fail if the
# file is being held open by a process, but there's only
# so much working around a lame I/O API one can take in
# a single day.
os.unlink(master_cache)
os.rename(tmpname, master_cache)
except IOError as e:
print("WARNING: Unable to write master message template to %s, proceeding without cache." % master_cache)
print("Cause: %s" % e)
return master_url
return master_cache_url
def local_template_filename():
"""Returns the message template's default location relative to template_verifier.py:
./messages/message_template.msg."""
d = os.path.dirname(os.path.realpath(__file__))
return os.path.join(d, 'messages', MESSAGE_TEMPLATE)
def getuser():
try:
# Unix-only.
import getpass
return getpass.getuser()
except ImportError:
import ctypes
MAX_PATH = 260 # according to a recent WinDef.h
name = ctypes.create_unicode_buffer(MAX_PATH)
namelen = ctypes.c_int(len(name)) # len in chars, NOT bytes
if not ctypes.windll.advapi32.GetUserNameW(name, ctypes.byref(namelen)):
raise ctypes.WinError()
return name.value
def local_master_cache_filename():
"""Returns the location of the master template cache (which is in the system tempdir)
<temp_dir>/master_message_template_cache.msg"""
import tempfile
d = tempfile.gettempdir()
user = getuser()
return os.path.join(d, 'master_message_template_cache.%s.msg' % user)
def run(sysargs):
parser = optparse.OptionParser(
usage="usage: %prog [FILE] [FILE]",
description=__doc__)
parser.add_option(
'-m', '--mode', type='string', dest='mode',
default='development',
help="""[development|production] The strictness mode to use
while checking the template; see the wiki page for details about
what is allowed and disallowed by each mode:
http://wiki.secondlife.com/wiki/Template_verifier.py
""")
parser.add_option(
'-u', '--master_url', type='string', dest='master_url',
default='https://github.com/secondlife/master-message-template/raw/master/message_template.msg',
help="""The url of the master message template.""")
parser.add_option(
'-c', '--cache_master', action='store_true', dest='cache_master',
default=False, help="""Set to true to attempt use local cached copy of the master template.""")
parser.add_option(
'-f', '--force', action='store_true', dest='force_verification',
default=False, help="""Set to true to skip the sha_1 check and force template verification.""")
options, args = parser.parse_args(sysargs)
if options.mode == 'production':
options.cache_master = False
# both current and master supplied in positional params
if len(args) == 2:
master_filename, current_filename = args
print("master:", master_filename)
print("current:", current_filename)
master_url = 'file://%s' % master_filename
current_url = 'file://%s' % current_filename
# only current supplied in positional param
elif len(args) == 1:
master_url = None
current_filename = args[0]
print("master:", options.master_url)
print("current:", current_filename)
current_url = 'file://%s' % current_filename
# nothing specified, use defaults for everything
elif len(args) == 0:
master_url = None
current_url = None
else:
die("Too many arguments")
if master_url is None:
master_url = options.master_url
if current_url is None:
current_filename = local_template_filename()
print("master:", options.master_url)
print("current:", current_filename)
current_url = 'file://%s' % current_filename
# retrieve the contents of the local template
current = fetch(current_url)
hexdigest = hashlib.sha1(current).hexdigest()
if not options.force_verification:
# Early exist if the template hasn't changed.
sha_url = "%s.sha1" % current_url
current_sha = fetch(sha_url).decode("utf-8")
if hexdigest == current_sha:
print("Message template SHA_1 has not changed.")
sys.exit(0)
# and check for syntax
current_parsed = llmessage.parseTemplateString(current.decode("utf-8"))
if options.cache_master:
# optionally return a url to a locally-cached master so we don't hit the network all the time
master_url = cache_master(master_url)
def parse_master_url():
master = fetch(master_url).decode("utf-8")
return llmessage.parseTemplateString(master)
try:
master_parsed = retry(3, parse_master_url)
except (IOError, tokenstream.ParseError) as e:
if options.mode == 'production':
raise e
else:
print("WARNING: problems retrieving the master from %s." % master_url)
print("Syntax-checking the local template ONLY, no compatibility check is being run.")
print("Cause: %s\n\n" % e)
return 0
acceptable, compat = compare(
master_parsed, current_parsed, options.mode)
def explain(header, compat):
print(header)
# indent compatibility explanation
print('\n\t'.join(compat.explain().split('\n')))
if acceptable:
explain("--- PASS ---", compat)
if options.force_verification == False:
print("Updating sha1 to %s" % hexdigest)
sha_filename = "%s.sha1" % current_filename
sha_file = open(sha_filename, 'w')
sha_file.write(hexdigest)
sha_file.close()
else:
explain("*** FAIL ***", compat)
return 1
if __name__ == '__main__':
sys.exit(run(sys.argv[1:]))
+30
View File
@@ -0,0 +1,30 @@
/**
* @file #filename#.cpp
* @brief Implementation of #filename#
* @author #getpass.getuser()#@lindenlab.com
*
* $LicenseInfo:firstyear=#datetime.datetime.now().year#&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) #datetime.datetime.now().year#, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#'' if ( skip_h ) else '%cinclude "%s.h"' % (35,filename)#
+31
View File
@@ -0,0 +1,31 @@
/**
* @file #filename#.h
* @brief Header file for #filename#
* @author #getpass.getuser()#@lindenlab.com
*
* $LicenseInfo:firstyear=#datetime.datetime.now().year#&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) #datetime.datetime.now().year#, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#'%c'%35#ifndef LL_#filename.upper().replace('-','_')#_H
#'%c'%35#define LL_#filename.upper().replace('-','_')#_H
#'%c'%35#endif // LL_#filename.upper().replace('-','_')#_H
@@ -0,0 +1,54 @@
list buttons = ["anim start", "anim stop", "step", "verbose on", "verbose off", " "];
string dialogInfo = "\nPlease make a choice.";
key ToucherID;
integer dialogChannel;
integer listenHandle;
integer commandChannel;
default
{
state_entry()
{
dialogChannel = -1 - (integer)("0x" + llGetSubString( (string)llGetKey(), -7, -1) );
commandChannel = -2001;
}
touch_start(integer num_detected)
{
ToucherID = llDetectedKey(0);
llListenRemove(listenHandle);
listenHandle = llListen(dialogChannel, "", ToucherID, "");
llDialog(ToucherID, dialogInfo, buttons, dialogChannel);
//llSetTimerEvent(60.0); // Here we set a time limit for responses
}
listen(integer channel, string name, key id, string message)
{
if (message == "-")
{
llDialog(ToucherID, dialogInfo, buttons, dialogChannel);
return;
}
llListenRemove(listenHandle);
// stop timer since the menu was clicked
llSetTimerEvent(0);
//llOwnerSay("Sending message " + message + " on channel " + (string)commandChannel);
llRegionSay(commandChannel, message);
}
timer()
{
// stop timer
llSetTimerEvent(0);
llListenRemove(listenHandle);
//llWhisper(0, "Sorry. You snooze; you lose.");
}
}
// Local Variables:
// shadow-file-name: "$SW_HOME/axon/scripts/testing/lsl/axon_test_region_driver.lsl"
// End:
@@ -0,0 +1,118 @@
integer listenHandle;
integer verbose;
integer current_animation_number;
string NowPlaying;
say_if_verbose(integer channel, string message)
{
if (verbose)
{
llSay(channel, message);
}
}
stop_all_animations()
{
integer count = llGetInventoryNumber(INVENTORY_ANIMATION);
string ItemName;
string NowPlaying;
while (count--)
{
ItemName = llGetInventoryName(INVENTORY_ANIMATION, count);
say_if_verbose(0, "Stopping " + ItemName);
llStopObjectAnimation(ItemName);
}
}
start_cycle_animations()
{
current_animation_number = llGetInventoryNumber(INVENTORY_ANIMATION);
next_animation(); // Do first iteration without waiting for timer
llSetTimerEvent(5.0);
}
next_animation()
{
string ItemName;
if (NowPlaying != "")
{
say_if_verbose(0, "Stopping " + NowPlaying);
llStopObjectAnimation(NowPlaying);
}
if (current_animation_number--)
{
ItemName = llGetInventoryName(INVENTORY_ANIMATION, current_animation_number);
say_if_verbose(0, "Starting " + ItemName);
llStartObjectAnimation(ItemName);
NowPlaying = ItemName;
}
else
{
// Start again at the top
current_animation_number = llGetInventoryNumber(INVENTORY_ANIMATION);
}
}
stop_cycle_animations()
{
llSetTimerEvent(0);
}
default
{
state_entry()
{
say_if_verbose(0, "Animated Object here");
listenHandle = llListen(-2001,"","","");
verbose = 0;
stop_all_animations();
}
listen(integer channel, string name, key id, string message)
{
//llOwnerSay("got message " + name + " " + (string) id + " " + message);
list words = llParseString2List(message,[" "],[]);
string command = llList2String(words,0);
string option = llList2String(words,1);
if (command=="anim")
{
stop_all_animations();
if (option=="start")
{
start_cycle_animations();
}
else if (option=="stop")
{
stop_cycle_animations();
}
}
if (command=="verbose")
{
if (option=="on")
{
verbose = 1;
}
else if (option=="off")
{
verbose = 0;
}
}
}
timer()
{
say_if_verbose(0, "timer triggered");
next_animation();
}
touch_start(integer total_number)
{
say_if_verbose(0, "Touch started.");
start_cycle_animations();
}
}
// Local Variables:
// shadow-file-name: "$SW_HOME/axon/scripts/testing/lsl/cycle_object_animations.lsl"
// End:
@@ -0,0 +1,133 @@
integer listenHandle;
integer verbose;
integer current_animation_number;
string NowPlaying;
say_if_verbose(integer channel, string message)
{
if (verbose)
{
llSay(channel, message);
}
}
stop_all_animations()
{
list curr_anims = llGetObjectAnimationNames();
say_if_verbose(0,"stopping all, curr_anims are " + (string) curr_anims);
integer length = llGetListLength(curr_anims);
integer index = 0;
while (index < length)
{
string anim = llList2String(curr_anims, index);
say_if_verbose(0, "Stopping " + anim);
llStopObjectAnimation(anim);
// This check isn't really needed, just included to demonstrate is_animation_running()
if (is_animation_running(anim))
{
say_if_verbose(0, "ERROR - failed to stop " + anim + "!");
}
++index;
}
}
integer is_animation_running(string anim)
{
list curr_anims = llGetObjectAnimationNames();
return ~llListFindList(curr_anims, (list)anim);
}
start_cycle_animations()
{
current_animation_number = llGetInventoryNumber(INVENTORY_ANIMATION);
next_animation(); // Do first iteration without waiting for timer
llSetTimerEvent(5.0);
}
next_animation()
{
string ItemName;
if (NowPlaying != "")
{
say_if_verbose(0, "Stopping " + NowPlaying);
llStopObjectAnimation(NowPlaying);
}
if (current_animation_number--)
{
ItemName = llGetInventoryName(INVENTORY_ANIMATION, current_animation_number);
say_if_verbose(0, "Starting " + ItemName);
llStartObjectAnimation(ItemName);
key anim_id = llGetInventoryKey(ItemName);
say_if_verbose(0, "Started item " + ItemName + " inv key " + (string) anim_id);
NowPlaying = ItemName;
}
else
{
// Start again at the top
current_animation_number = llGetInventoryNumber(INVENTORY_ANIMATION);
}
}
stop_cycle_animations()
{
llSetTimerEvent(0);
}
default
{
state_entry()
{
say_if_verbose(0, "Animated Object here");
listenHandle = llListen(-2001,"","","");
verbose = 0;
stop_all_animations();
}
listen(integer channel, string name, key id, string message)
{
//llOwnerSay("got message " + name + " " + (string) id + " " + message);
list words = llParseString2List(message,[" "],[]);
string command = llList2String(words,0);
string option = llList2String(words,1);
if (command=="anim")
{
stop_all_animations();
if (option=="start")
{
start_cycle_animations();
}
else if (option=="stop")
{
stop_cycle_animations();
}
}
if (command=="verbose")
{
if (option=="on")
{
verbose = 1;
}
else if (option=="off")
{
verbose = 0;
}
}
}
timer()
{
say_if_verbose(0, "timer triggered");
next_animation();
}
touch_start(integer total_number)
{
say_if_verbose(0, "Touch started.");
start_cycle_animations();
}
}
// Local Variables:
// shadow-file-name: "$SW_HOME/axon/scripts/testing/lsl/cycle_object_animations_v2.lsl"
// End:
@@ -0,0 +1,90 @@
integer listenHandle;
integer verbose;
integer num_steps = 12;
float circle_time = 5.0;
integer circle_step;
vector circle_pos;
vector circle_center;
float circle_radius;
start_circle(vector center, float radius)
{
vector currentPosition = llGetPos();
circle_center = center;
circle_radius = radius;
circle_step = 0;
llSetTimerEvent(circle_time/num_steps);
llTargetOmega(<0.0, 0.0, 1.0>, TWO_PI/circle_time, 1.0);
}
stop_circle()
{
llSetTimerEvent(0);
llTargetOmega(<0.0, 0.0, 1.0>, TWO_PI/circle_time, 0.0);
llSetRegionPos(circle_center);
}
next_circle()
{
float rad = (circle_step * TWO_PI)/num_steps;
float x = circle_center.x + llCos(rad)*circle_radius;
float y = circle_center.y + llSin(rad)*circle_radius;
float z = circle_center.z;
llSetRegionPos(<x,y,z>);
circle_step = (circle_step+1)%num_steps;
}
default
{
state_entry()
{
//llSay(0, "Hello, Avatar!");
listenHandle = llListen(-2001,"","","");
verbose = 0;
circle_center = llGetPos();
}
listen(integer channel, string name, key id, string message)
{
//llOwnerSay("got message " + name + " " + (string) id + " " + message);
list words = llParseString2List(message,[" "],[]);
string command = llList2String(words,0);
string option = llList2String(words,1);
if (command=="anim")
{
if (option=="start")
{
start_circle(llGetPos(), 3.0);
}
else if (option=="stop")
{
stop_circle();
}
}
if (command=="verbose")
{
if (option=="on")
{
verbose = 1;
}
else if (option=="off")
{
verbose = 0;
}
}
if (command=="step")
{
llSetTimerEvent(0);
next_circle();
}
}
timer()
{
next_circle();
}
}
// Local Variables:
// shadow-file-name: "$SW_HOME/axon/scripts/testing/lsl/move_in_circle_using_llSetRegionPos.lsl"
// End: