Best Practices
Code Organization
Shader Profile Management
DO: Define explicit shader profiles for known shaders
["Shaders/SkinnedMesh/Diffuse_Tint"] = new ShaderProfile
{
ColorTintParam = "Tint_Color", // Explicit, shader-specific
DiffuseTexture = "Diffuse_Texture",
HasBloom = false
}DON'T: Use generic fallbacks in DefaultProfile
// ❌ BAD
DefaultProfile = new ShaderProfile
{
ColorTintParam = "Color", // Too generic!
...
}Parameter Resolution
DO: Check multiple parameter name variations
BloomStrength = TryGetFloat(profile.BloomIntensityParam)
?? TryGetFloat(DefaultProfile.BloomIntensityParam)
?? TryGetFloat("BloomIntensity") // Variation
?? TryGetFloat("Emissive_Strength") // Alternative
?? 0;DON'T: Only check one parameter name
// ❌ BAD - Misses variations
BloomStrength = TryGetFloat("Bloom_Strength") ?? 0;Texture Handling
DO: Always clone before modifying
const clonedMap = material.map.clone();
material.map = clonedMap;
clonedMap.repeat.set(newRepeatX, newRepeatY);DON'T: Modify shared instances
// ❌ BAD - Modifies shared texture
material.map.repeat.set(newRepeatX, newRepeatY);Procedural Shader Handling
DO: Generate placeholder textures
if (magickImage == null && staticMaterialForTexture != null &&
staticMaterialForTexture.IsProceduralShader())
{
// Generate 1x1 texture from color params
magickImage = new MagickImage(MagickColor.FromRgb(r, g, b), 1, 1);
}DON'T: Skip material creation
// ❌ BAD - Material never created
if (magickImage == null) {
continue;
}Testing
Always Test These Cases
- Soraka Skin16: Previously had incorrect red tint on body (generic "Color" param)
- Renata Skin0: Has
Weapon_GlassandCanister_Glass(procedural shaders) - Annie Skin31: Has time-based auto-animated flipbooks on body
- Milio Skin0: Has animation-synced flipbooks on eyes/mouth
Verification Scripts
Use the Python scripts in common-pitfalls.md to verify:
- No incorrect
baseColorFactortinting - Glass materials have
isGlass: trueandglassParams - Flipbook materials have
flipbookSpeedwhen needed
Documentation
When Adding New Shader Profiles
- Check
shaders.bin.jsonfor parameter names - Verify parameter usage (is it for base tint or VFX?)
- Add explicit profile in
StaticMaterial.cs - Test with actual skin that uses the shader
- Document in this file if it's a special case
When Fixing Bugs
- Document the mistake in
common-pitfalls.md - Add verification steps
- Update this file with the correct pattern
- Add to daily log
Frontend Material Handling
Glass Material Conversion
DO: Convert to MeshPhysicalMaterial with all parameters
if (userData.isGlass) {
const glassMaterial = new MeshPhysicalMaterial({
color: originalMaterial.color,
map: originalMaterial.map,
transparent: true,
opacity: opacity,
transmission: transmission,
roughness: roughness,
ior: ior,
thickness: thickness,
depthWrite: false,
});
object.material = glassMaterial;
}Flipbook Animation
DO: Handle both animation-synced and time-based
// Animation-synced (from keyframes)
if (flipbookAccessoryHash) {
// Update from animation keyframes
}
// Time-based (auto-animated)
if (flipbookSpeed) {
// Update based on elapsed time
elapsedTime += delta;
const frameDuration = 1 / flipbookSpeed;
if (elapsedTime >= frameDuration) {
// Advance frame
}
}Loop Grace Period (Model Viewer Engine)
- Problem: Submesh visibility flickers when animations loop (events restart at frame 0).
- Fix: Track
lastVisibilityFrame; detect loop whencurrentFrame < lastVisibilityFrame - 1; setloopGraceFrames ≈ 2.5to skip visibility updates for a few frames after a loop. - Reset: Clear
lastVisibilityFrameandloopGraceFrameson model load, animation start, and stop. - Scope: Only affects models with
submeshVisibilityEvents; guarded, so no overhead elsewhere.
Extractor-Specific Practices
Hash Table Management
DO: Always handle hash table loading failures gracefully
if (!await HashTables.TryLoadLatest(logger))
{
logger.Warning("Failed to load latest hash tables, trying cached...");
if (!await HashTables.TryLoadExisting(path, logger))
{
logger.Fatal("No hash tables available");
return false;
}
}DON'T: Assume hash tables are always available
// ❌ BAD - Will crash if hash tables not loaded
var path = HashTables.Game[hash];WAD Downloading
DO: Clear PBE cache to ensure fresh downloads
if (patchline.Equals("pbe", StringComparison.OrdinalIgnoreCase))
{
// Clear cache for PBE to get latest content
if (File.Exists(cachedPath))
File.Delete(cachedPath);
}DON'T: Use stale PBE cache
// ❌ BAD - PBE changes frequently, cache may be outdated
if (File.Exists(cachedPath))
return cachedPath; // Without checking patchlinePath Resolution
DO: Support both file paths and champion names
if (LeaguePathResolver.IsFilePath(input))
{
// Use as file path
}
else
{
// Resolve champion name to WAD path
var resolved = await LeaguePathResolver.ResolveInputAsync(input, ...);
}DON'T: Assume input is always a file path
// ❌ BAD - Fails if user provides champion name
var wad = new StringWad(input);Error Handling
DO: Provide clear error messages with context
logger.Fatal("Couldn't open '{Path}'. Ensure the WAD file exists and is not corrupted.", filePath);DON'T: Use generic error messages
// ❌ BAD - Not helpful
logger.Fatal("Error");Error Prevention
Before Making Changes
- Read
common-pitfalls.md - Check if similar changes were made before (daily logs)
- Test with known problematic skins
- Verify no regressions
- Test with both PBE and live patchlines
- Verify hash tables load correctly
Code Review Checklist
- [ ] No generic parameter fallbacks
- [ ] Textures cloned before modification
- [ ] Procedural shaders handled correctly
- [ ] Multiple parameter name variations checked
- [ ] Transparency uses both
alphaModeandbaseColorFactor - [ ] Flipbook speed extracted when needed
- [ ] Hash table loading handled gracefully
- [ ] PBE cache cleared appropriately
- [ ] Path resolution supports both files and names