- Winner Glow Effects: Bright green glow on both scoreboard and floaters for winners - Extended Victory Display: 24-second celebration timer before game reset - Enhanced Visual Feedback: Color-coded glow system (green/yellow/red) - Victory Text Display: Winner floaters show 'ULTIMATE VICTORY!' and 'ULTIMATE SURVIVOR' - Timer System Optimization: Dedicated victory timer mode, removed unused timeout system - Memory Efficiency: Cleaned up unused components for better performance - Synchronized Celebration: Both scoreboard and floater systems celebrate winner simultaneously - Visual Priority: Winner glow overrides peril glow during victory celebration Professional winner recognition system makes victory feel rewarding and celebrated!
512 lines
No EOL
22 KiB
Python
Executable file
512 lines
No EOL
22 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""
|
||
LSL Validator - Simple syntax and style checker for Linden Scripting Language files
|
||
Created for the Peril Dice Game project
|
||
|
||
Usage:
|
||
python3 lsl_validator.py filename.lsl # Validate single file
|
||
python3 lsl_validator.py . # Validate all .lsl files in directory
|
||
python3 lsl_validator.py /path/to/dir # Validate all .lsl files in specified directory
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
import glob
|
||
from pathlib import Path
|
||
|
||
class LSLValidator:
|
||
def __init__(self):
|
||
self.errors = []
|
||
self.warnings = []
|
||
self.info = []
|
||
|
||
# Common LSL functions and constants for basic validation
|
||
self.lsl_functions = {
|
||
# State management
|
||
'default', 'state_entry', 'state_exit', 'on_rez',
|
||
# Events
|
||
'touch_start', 'touch_end', 'listen', 'timer', 'link_message',
|
||
'collision_start', 'collision', 'collision_end', 'http_response',
|
||
# String functions
|
||
'llStringLength', 'llGetSubString', 'llSubStringIndex', 'llToUpper', 'llToLower',
|
||
'llMD5String', 'llSHA1String', 'llEscapeURL', 'llUnescapeURL',
|
||
# List functions
|
||
'llGetListLength', 'llList2String', 'llList2Integer', 'llList2Float', 'llList2Key',
|
||
'llList2Vector', 'llList2Rot', 'llDeleteSubList', 'llListReplaceList',
|
||
'llListFindList', 'llListInsertList', 'llCSV2List', 'llList2CSV',
|
||
'llParseString2List', 'llDumpList2String', 'llParseStringKeepNulls',
|
||
# Communication
|
||
'llSay', 'llWhisper', 'llShout', 'llRegionSay', 'llOwnerSay',
|
||
'llListen', 'llListenRemove', 'llDialog', 'llMessageLinked',
|
||
# Object functions
|
||
'llSetText', 'llSetTexture', 'llGetTexture', 'llSetAlpha', 'llGetAlpha',
|
||
'llSetColor', 'llGetColor', 'llSetScale', 'llGetScale',
|
||
'llSetPos', 'llGetPos', 'llSetRot', 'llGetRot',
|
||
# Avatar functions
|
||
'llKey2Name', 'llGetDisplayName', 'llRequestDisplayName',
|
||
'llGetOwner', 'llDetectedKey', 'llDetectedName', 'llGetAgentSize',
|
||
# Time functions
|
||
'llGetUnixTime', 'llGetTimestamp', 'llSleep', 'llSetTimerEvent',
|
||
# Memory functions
|
||
'llGetUsedMemory', 'llGetFreeMemory', 'llResetScript',
|
||
# Math functions
|
||
'llAbs', 'llCeil', 'llFloor', 'llRound', 'llSqrt', 'llPow',
|
||
'llSin', 'llCos', 'llTan', 'llAsin', 'llAcos', 'llAtan2',
|
||
'llFrand', 'llGenerateKey',
|
||
# Type conversion
|
||
'(string)', '(integer)', '(float)', '(key)', '(vector)', '(rotation)',
|
||
}
|
||
|
||
self.lsl_constants = {
|
||
'TRUE', 'FALSE', 'NULL_KEY', 'EOF', 'ZERO_VECTOR', 'ZERO_ROTATION',
|
||
'PI', 'PI_BY_TWO', 'TWO_PI', 'DEG_TO_RAD', 'RAD_TO_DEG',
|
||
'LINK_ROOT', 'LINK_SET', 'LINK_ALL_OTHERS', 'LINK_ALL_CHILDREN', 'LINK_THIS',
|
||
}
|
||
|
||
# Keywords that are definitely forbidden in LSL (conservative list)
|
||
self.forbidden_keywords = {
|
||
'break', 'continue', 'switch', 'case', 'goto',
|
||
'class', 'struct', 'enum', 'union', 'namespace'
|
||
}
|
||
|
||
# LSL data types
|
||
self.lsl_types = {
|
||
'integer', 'float', 'string', 'key', 'vector', 'rotation', 'list'
|
||
}
|
||
|
||
# Scope tracking for variables
|
||
self.global_vars = set() # Global variables
|
||
self.local_vars = [] # Stack of local scopes
|
||
self.current_function = None
|
||
|
||
def validate_file(self, filepath):
|
||
"""Validate a single LSL file"""
|
||
self.errors = []
|
||
self.warnings = []
|
||
self.info = []
|
||
|
||
try:
|
||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||
content = f.read()
|
||
lines = content.split('\n')
|
||
|
||
self._check_syntax(lines, filepath)
|
||
self._check_style(lines, filepath)
|
||
self._check_scope(lines, filepath)
|
||
self._check_performance(content, filepath)
|
||
self._check_memory_usage(content, filepath)
|
||
|
||
return self._generate_report(filepath)
|
||
|
||
except FileNotFoundError:
|
||
return f"❌ ERROR: File not found: {filepath}"
|
||
except Exception as e:
|
||
return f"❌ ERROR: Failed to validate {filepath}: {str(e)}"
|
||
|
||
def _check_syntax(self, lines, filepath):
|
||
"""Check for basic syntax errors"""
|
||
brace_count = 0
|
||
paren_count = 0
|
||
in_string = False
|
||
in_comment = False
|
||
|
||
for line_num, line in enumerate(lines, 1):
|
||
original_line = line
|
||
line = line.strip()
|
||
|
||
# Skip empty lines
|
||
if not line:
|
||
continue
|
||
|
||
# Track multi-line comments
|
||
if '/*' in line and '*/' not in line:
|
||
in_comment = True
|
||
continue
|
||
elif '*/' in line and in_comment:
|
||
in_comment = False
|
||
continue
|
||
elif in_comment:
|
||
continue
|
||
|
||
# Skip single-line comments
|
||
if line.startswith('//'):
|
||
continue
|
||
|
||
# Check for common syntax errors
|
||
if line.endswith(';') and line.count(';') > 1:
|
||
if not any(keyword in line for keyword in ['for', 'if', 'while']):
|
||
self.warnings.append(f"Line {line_num}: Multiple statements on one line")
|
||
|
||
# Check brace balance
|
||
brace_count += line.count('{') - line.count('}')
|
||
if brace_count < 0:
|
||
self.errors.append(f"Line {line_num}: Unmatched closing brace")
|
||
|
||
# Check parentheses balance
|
||
paren_count += line.count('(') - line.count(')')
|
||
|
||
# Check for missing semicolons (basic check)
|
||
if (line.endswith(')') and not line.startswith('if') and not line.startswith('while')
|
||
and not line.startswith('for') and not line.endswith(');') and '{' not in line
|
||
and '}' not in line and not line.startswith('state') and not line.startswith('default')):
|
||
self.warnings.append(f"Line {line_num}: Possible missing semicolon")
|
||
|
||
# Check for forbidden LSL keywords
|
||
self._check_forbidden_keywords(line, line_num)
|
||
|
||
# Final brace balance check
|
||
if brace_count != 0:
|
||
self.errors.append(f"Unmatched braces: {brace_count} excess opening braces" if brace_count > 0 else f"{-brace_count} missing opening braces")
|
||
|
||
def _check_forbidden_keywords(self, line, line_num):
|
||
"""Check for keywords that are forbidden in LSL"""
|
||
# Skip comments
|
||
if '//' in line:
|
||
line = line[:line.index('//')]
|
||
|
||
# Remove strings to avoid false positives
|
||
# Simple approach: remove content between quotes
|
||
import re
|
||
line_no_strings = re.sub(r'"[^"]*"', '""', line)
|
||
|
||
# Check each forbidden keyword
|
||
for keyword in self.forbidden_keywords:
|
||
# Use word boundaries to avoid matching substrings
|
||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||
if re.search(pattern, line_no_strings):
|
||
self.errors.append(f"Line {line_num}: '{keyword}' is not supported in LSL")
|
||
|
||
def _check_scope(self, lines, filepath):
|
||
"""Check for variable scope issues"""
|
||
# Reset scope tracking
|
||
self.global_vars = set()
|
||
self.local_vars = []
|
||
self.current_function = None
|
||
|
||
in_function = False
|
||
brace_depth = 0
|
||
|
||
for line_num, line in enumerate(lines, 1):
|
||
original_line = line
|
||
line = line.strip()
|
||
|
||
# Skip empty lines and comments
|
||
if not line or line.startswith('//') or line.startswith('/*'):
|
||
continue
|
||
|
||
# Remove comments from line
|
||
if '//' in line:
|
||
line = line[:line.index('//')]
|
||
line = line.strip()
|
||
|
||
# Track brace depth for scope management
|
||
open_braces = line.count('{')
|
||
close_braces = line.count('}')
|
||
|
||
# Check for function definitions
|
||
if not in_function and ('(' in line and ')' in line and '{' in line):
|
||
# This might be a function definition
|
||
if any(event in line for event in ['state_entry', 'touch_start', 'listen', 'timer', 'on_rez', 'link_message', 'http_response']):
|
||
in_function = True
|
||
self.local_vars.append(set()) # New local scope
|
||
|
||
# Handle scope changes
|
||
if open_braces > 0:
|
||
for _ in range(open_braces):
|
||
brace_depth += 1
|
||
if in_function and brace_depth > 1: # Nested scope within function
|
||
self.local_vars.append(set())
|
||
|
||
# Check for variable declarations
|
||
self._check_variable_declaration(line, line_num, in_function)
|
||
|
||
# Check for variable usage
|
||
self._check_variable_usage(line, line_num)
|
||
|
||
# Handle closing braces
|
||
if close_braces > 0:
|
||
for _ in range(close_braces):
|
||
brace_depth -= 1
|
||
if in_function and len(self.local_vars) > 1:
|
||
self.local_vars.pop() # Exit nested scope
|
||
elif brace_depth == 0 and in_function:
|
||
# Exiting function
|
||
in_function = False
|
||
self.local_vars.clear()
|
||
|
||
def _check_variable_declaration(self, line, line_num, in_function):
|
||
"""Check for variable declarations"""
|
||
import re
|
||
|
||
# Remove string literals to avoid false matches
|
||
line_no_strings = re.sub(r'"[^"]*"', '""', line)
|
||
|
||
# Pattern to match LSL variable declarations: type varname;
|
||
for lsl_type in self.lsl_types:
|
||
# Handle special case of for loop declarations: for(integer i = 0; ...)
|
||
for_loop_pattern = rf'for\s*\(\s*{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*='
|
||
for_matches = re.findall(for_loop_pattern, line_no_strings)
|
||
|
||
for var_name in for_matches:
|
||
if var_name.startswith('ll') or var_name in self.lsl_functions:
|
||
continue
|
||
# For loop variables are always local and shouldn't conflict with globals
|
||
if len(self.local_vars) > 0:
|
||
self.local_vars[-1].add(var_name)
|
||
continue
|
||
|
||
# Regular variable declarations: type identifier; or type identifier =
|
||
# Skip for loop variable declarations by checking the line content
|
||
if 'for(' not in line_no_strings.replace(' ', '') and 'for (' not in line_no_strings:
|
||
pattern = rf'\b{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[;=]'
|
||
matches = re.findall(pattern, line_no_strings)
|
||
else:
|
||
matches = []
|
||
|
||
for var_name in matches:
|
||
# Skip LSL built-in functions that might match the pattern
|
||
if var_name.startswith('ll') or var_name in self.lsl_functions:
|
||
continue
|
||
|
||
# Skip common parameter names that aren't real declarations
|
||
if var_name in {'total_number', 'sender', 'num', 'str', 'id', 'start_param',
|
||
'channel', 'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body'}:
|
||
continue
|
||
|
||
if in_function:
|
||
# Check if already declared in current local scope
|
||
if len(self.local_vars) > 0 and var_name in self.local_vars[-1]:
|
||
self.errors.append(f"Line {line_num}: Variable '{var_name}' already declared in this scope")
|
||
else:
|
||
# Add to current local scope
|
||
if len(self.local_vars) > 0:
|
||
self.local_vars[-1].add(var_name)
|
||
else:
|
||
# Global variable - only flag if it's a real redeclaration
|
||
if var_name in self.global_vars:
|
||
# Only error if this looks like a real declaration line
|
||
if ';' in line or '=' in line:
|
||
self.errors.append(f"Line {line_num}: Global variable '{var_name}' already declared")
|
||
else:
|
||
self.global_vars.add(var_name)
|
||
|
||
def _check_variable_usage(self, line, line_num):
|
||
"""Check for usage of undefined variables (conservative approach)"""
|
||
import re
|
||
|
||
# Skip variable declarations and function definitions
|
||
if any(lsl_type in line for lsl_type in self.lsl_types):
|
||
return
|
||
|
||
# Skip comments and string literals
|
||
if line.strip().startswith('//') or line.strip().startswith('/*') or '"' in line:
|
||
return
|
||
|
||
# Skip lines that are clearly not variable usage
|
||
if (line.startswith('state') or line.startswith('#') or line.strip() == 'default' or
|
||
'llOwnerSay' in line or 'llSay' in line or 'llMessageLinked' in line or
|
||
('{' in line and '}' in line and line.count('{') == line.count('}'))):
|
||
return
|
||
|
||
# Only check very specific patterns where we're confident it's a variable usage
|
||
# Pattern 1: variable assignment: varname = something
|
||
assignment_vars = re.findall(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=', line)
|
||
|
||
# Pattern 2: variable in arithmetic: something + varname or varname + something
|
||
arithmetic_vars = re.findall(r'(?:^|[+\-*/=\s])([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*[+\-*/]|\s*;|$)', line)
|
||
|
||
# Combine patterns
|
||
potential_vars = set(assignment_vars + arithmetic_vars)
|
||
|
||
for var_name in potential_vars:
|
||
# Skip LSL functions, constants, keywords, and common false positives
|
||
if (var_name in self.lsl_functions or var_name in self.lsl_constants or
|
||
var_name in self.lsl_types or var_name.startswith('ll') or
|
||
var_name in {'if', 'else', 'for', 'while', 'do', 'return', 'jump', 'state', 'default',
|
||
'total_number', 'sender', 'num', 'str', 'id', 'start_param', 'channel',
|
||
'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body',
|
||
# Common English words that show up in strings/comments
|
||
'the', 'and', 'or', 'not', 'has', 'been', 'with', 'from', 'this', 'that',
|
||
'will', 'can', 'may', 'all', 'any', 'some', 'new', 'old', 'get', 'set'} or
|
||
var_name.isdigit() or len(var_name) <= 2):
|
||
continue
|
||
|
||
# Check if variable is defined
|
||
is_defined = var_name in self.global_vars
|
||
|
||
# Check local scopes (from innermost to outermost)
|
||
if not is_defined:
|
||
for scope in reversed(self.local_vars):
|
||
if var_name in scope:
|
||
is_defined = True
|
||
break
|
||
|
||
# Only warn for very likely variable usage patterns
|
||
if not is_defined:
|
||
# Must be lowercase and used in assignment or arithmetic context
|
||
if (var_name.islower() and
|
||
(var_name in assignment_vars or
|
||
(var_name in arithmetic_vars and any(op in line for op in ['+', '-', '*', '/'])))):
|
||
self.warnings.append(f"Line {line_num}: Variable '{var_name}' may not be defined")
|
||
|
||
def _check_style(self, lines, filepath):
|
||
"""Check for style and best practice issues"""
|
||
for line_num, line in enumerate(lines, 1):
|
||
stripped = line.strip()
|
||
|
||
# Skip comments and empty lines
|
||
if not stripped or stripped.startswith('//') or stripped.startswith('/*'):
|
||
continue
|
||
|
||
# Check for very long lines
|
||
if len(line) > 120:
|
||
self.warnings.append(f"Line {line_num}: Line too long ({len(line)} characters)")
|
||
|
||
# Check for tabs vs spaces (LSL prefers spaces)
|
||
if '\t' in line:
|
||
self.info.append(f"Line {line_num}: Contains tabs (LSL prefers spaces)")
|
||
|
||
# Check for deprecated or problematic patterns
|
||
if 'llOwnerSay' in stripped and 'DEBUG' in stripped.upper():
|
||
self.info.append(f"Line {line_num}: Debug message found - consider removing for production")
|
||
|
||
def _check_performance(self, content, filepath):
|
||
"""Check for performance issues"""
|
||
# Check for expensive operations in loops
|
||
lines = content.split('\n')
|
||
in_loop = False
|
||
loop_indent = 0
|
||
|
||
for line_num, line in enumerate(lines, 1):
|
||
stripped = line.strip()
|
||
current_indent = len(line) - len(line.lstrip())
|
||
|
||
# Detect loop start
|
||
if any(keyword in stripped for keyword in ['for', 'while', 'do']):
|
||
in_loop = True
|
||
loop_indent = current_indent
|
||
|
||
# Detect loop end
|
||
elif in_loop and stripped == '}' and current_indent <= loop_indent:
|
||
in_loop = False
|
||
|
||
# Check for expensive operations in loops
|
||
elif in_loop:
|
||
if 'llList2CSV' in stripped or 'llCSV2List' in stripped:
|
||
self.warnings.append(f"Line {line_num}: Expensive CSV operation in loop")
|
||
if 'llParseString2List' in stripped:
|
||
self.warnings.append(f"Line {line_num}: String parsing in loop may be expensive")
|
||
|
||
def _check_memory_usage(self, content, filepath):
|
||
"""Check for potential memory issues"""
|
||
# Count string operations
|
||
csv_ops = content.count('llList2CSV') + content.count('llCSV2List')
|
||
if csv_ops > 10:
|
||
self.warnings.append(f"High number of CSV operations ({csv_ops}) - may impact memory")
|
||
|
||
# Check for large string concatenations
|
||
if content.count(' + ') > 50:
|
||
self.warnings.append("High number of string concatenations - consider optimizing")
|
||
|
||
# Check for memory monitoring
|
||
if 'llGetFreeMemory' in content:
|
||
self.info.append("✅ Memory monitoring detected")
|
||
|
||
# Check for flood protection
|
||
if 'cooldown' in content.lower() or 'flood' in content.lower():
|
||
self.info.append("✅ Flood protection detected")
|
||
|
||
def _generate_report(self, filepath):
|
||
"""Generate validation report"""
|
||
filename = os.path.basename(filepath)
|
||
report = [f"\n🔍 LSL Validation Report: {filename}"]
|
||
report.append("=" * 60)
|
||
|
||
if not self.errors and not self.warnings:
|
||
report.append("✅ No issues found!")
|
||
else:
|
||
if self.errors:
|
||
report.append(f"\n❌ ERRORS ({len(self.errors)}):")
|
||
for error in self.errors:
|
||
report.append(f" {error}")
|
||
|
||
if self.warnings:
|
||
report.append(f"\n⚠️ WARNINGS ({len(self.warnings)}):")
|
||
for warning in self.warnings:
|
||
report.append(f" {warning}")
|
||
|
||
if self.info:
|
||
report.append(f"\nℹ️ INFO ({len(self.info)}):")
|
||
for info in self.info:
|
||
report.append(f" {info}")
|
||
|
||
# File stats
|
||
try:
|
||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||
content = f.read()
|
||
lines = len(content.split('\n'))
|
||
chars = len(content)
|
||
|
||
report.append(f"\n📊 FILE STATS:")
|
||
report.append(f" Lines: {lines}")
|
||
report.append(f" Characters: {chars:,}")
|
||
report.append(f" Size: {os.path.getsize(filepath):,} bytes")
|
||
|
||
except:
|
||
pass
|
||
|
||
return '\n'.join(report)
|
||
|
||
def main():
|
||
if len(sys.argv) != 2:
|
||
print("Usage: python3 lsl_validator.py <file.lsl|directory>")
|
||
sys.exit(1)
|
||
|
||
target = sys.argv[1]
|
||
validator = LSLValidator()
|
||
|
||
if os.path.isfile(target):
|
||
# Single file validation
|
||
if not target.endswith('.lsl'):
|
||
print("❌ ERROR: File must have .lsl extension")
|
||
sys.exit(1)
|
||
|
||
result = validator.validate_file(target)
|
||
print(result)
|
||
|
||
elif os.path.isdir(target):
|
||
# Directory validation
|
||
lsl_files = glob.glob(os.path.join(target, "*.lsl"))
|
||
|
||
if not lsl_files:
|
||
print(f"❌ No .lsl files found in {target}")
|
||
sys.exit(1)
|
||
|
||
print(f"🔍 Validating {len(lsl_files)} LSL files in {target}")
|
||
print("=" * 80)
|
||
|
||
total_errors = 0
|
||
total_warnings = 0
|
||
|
||
for lsl_file in sorted(lsl_files):
|
||
result = validator.validate_file(lsl_file)
|
||
print(result)
|
||
|
||
total_errors += len(validator.errors)
|
||
total_warnings += len(validator.warnings)
|
||
|
||
print("\n" + "=" * 80)
|
||
print(f"📋 SUMMARY: {len(lsl_files)} files validated")
|
||
print(f" Total Errors: {total_errors}")
|
||
print(f" Total Warnings: {total_warnings}")
|
||
|
||
if total_errors > 0:
|
||
sys.exit(1)
|
||
|
||
else:
|
||
print(f"❌ ERROR: {target} is not a file or directory")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main() |