{
	Hotkey: Ctrl+Alt+G
}
unit BDAssetLoader;

interface
implementation
uses xEditAPI, Classes, SysUtils, StrUtils, Windows;

const
    LOGLEVEL = 10;
    LIMIT = HighInteger;
    USE_CACHE = false;
    yaFileName = 'YiffyAgeConsolidated.esp';
    SEP = '\';

    // Max actor races supported. All the arrays holding race info use this.
    MAX_RACES = 100; 
    MAX_PRESETS = 10000;

    MALE = 0;
    FEMALE = 1;

    // Max number of head types the game supports. Used in raceHeadparts.
    HEADPART_TYPES_COUNT = 7;

    // Skin layers
    TINTLAYERS_COUNT = 26;

type TTintPreset = record
    presetColor: IwbMainRecord;
    defaultValue: float;
    presetIndex: integer;
    end;

//type TIntPresetArray = array of TTintPreset;

type TSkinTintLayer = Record
    TINI: integer;
    maskType: string;
    element: IwbELement;
    presetsStart: integer;
    presetsCount: integer;
    end;

// type TTintLayers = Record
//     layers[0..13 {TINTLAYERS_COUNT}];
//     paintCount: integer;
//     paintStart: integer;
//     dirtCount: integer;
//     dirtStart: integer;
//     end;

type TRaceInfo = Record
    mainRecord: IwbMainRecord;
    tints: array [0..25 {TINTLAYERS_COUNT}] of TSkinTintLayer;
    headparts: array[{hp} 0..7] of TStringList;
    maskCount: integer;
    muzzleCount: integer;
    end;

var
    // Cached game assets so we don't have to go back to the database.
    // Need to check if this is really faster.
    gameAssetsStr: TStringList;
    gameAssetsElem: TStringList;

    skyrimFile: IwbFile;
    yaFile: IwbFile;

    // Store all race headparts. First index is 1:1 with the "races" stringlist.
    // There must not be more than MAX_RACES. Delphi won't let us make this a
    // dynamic array and won't let us use the Const in the array declaration, so
    // we're stuck with this. hp index is 1:1 with "headpartsList". 
    raceInfo: array[0..100 {MAX_RACES}, 0..2 {sex}] of TRaceInfo;

    // All actor races, collected from the load order.
    masterRaceList: TStringList;

    // All headpart types--translates between the headpart type string and 
    // an index.
    headpartsList: TStringList;

    // Add headpart names to this list and the Object will be set to the headpart
    // element for quick reference
    specialHeadparts: TStringList;

    // Initialized to be indices into headpartsList for text-to-index translations
    HEADPART_EYEBROWS: integer;
    HEADPART_EYES: integer;
    HEADPART_FACE: integer;
    HEADPART_FACIAL_HAIR: integer;
    HEADPART_HAIR: integer;
    HEADPART_MISC: integer;
    HEADPART_SCAR: integer;

    tintlayersList: TStringList;

    // Initialized to be indices into tintlayersList
    TL_CHEEK_COLOR_LOWER: integer;
    TL_CHEEK_COLOR: integer;
    TL_CHIN: integer;
    TL_EAR: integer;
    TL_EYESOCKET_LOWER: integer;
    TL_EYESOCKET_UPPER: integer;
    TL_EYELINER: integer;
    TL_FOREHEAD: integer;
    TL_LAUGH_LINES: integer;
    TL_LIP_COLOR: integer;
    TL_NECK: integer;
    TL_NOSE: integer;
    TL_OLD: integer;
    TL_SKIN_TONE: integer;
    TL_MASK: integer;
    TL_MUZZLE: integer;

    // If any errors occurred during run.
    errCount: integer;
    logIndent: integer;

// =============================================================================

Procedure Log(importance: integer; txt: string);
var
    i: integer;
    s: string;
begin
    s := '';
	if importance <= LOGLEVEL then begin
        if LeftStr(txt, 1) = '>' then dec(logIndent);
        for i := 1 to logIndent do s := s + '|   ';
        AddMessage(s + txt);
        if LeftStr(txt, 1) = '<' then inc(logIndent);
    end;
end;

procedure Err(txt: string);
begin
    AddMessage('ERROR: ' + txt);
    inc(errCount);
end;

function FormName(e: IwbMainRecord): string;
begin
    Result := EditorID(e) + ' [ ' + IntToHex(FormID(e),8) + ' ]';
end;

function GetAttr(name: string): string;
begin
    Result := gameAssetsStr.ValueFromIndex[gameAssetsStr.IndexOfName(name)];
end;

procedure SetAttr(name, value: string);
begin
    gameAssetsStr.Add(name + '=' + value);
end;

function GetElemAttr(name: string): IwbElement;
begin
    Result := ObjectToElement(gameAssetsElem.Objects[gameAssetsElem.IndexOf(name)]);
end;

procedure SetElemAttr(name: string; value: IInterface);
begin
    gameAssetsElem.AddObject(name, value);
end;

function GetAssetElem(t, n, a: string): IInterface;
begin
    s := t + SEP + id + SEP + p;
    i := gameAssetsElem.IndexOf(s);
    if i >= 0 then
        Result := gameAssetsElem.Objects[i]
    else 
        Result := nil;
end;

procedure SetAssetElem(t, n, a: string; v: IInterface);
begin
    s := t + SEP + id + SEP + p;
    gameAssetsElem.AddObject(s, v);
end;

function GetGameAssetStr(f: IwbFile; t: string; id: string; p: string): string;
var
    s: string;
    i: integer;
    v: string;
begin
    s := GetFileName(f) + t + SEP + id + SEP + p;
    i := gameAssetsStr.IndexOfName(s);
    if USE_CACHE and (i >= 0) then
        Result := gameAssetsStr.ValueFromIndex[i]
    else begin
        Log(10, '*');
        v := GetElementEditValues(MainRecordByEditorID(GroupBySignature(f, t), id), p);
        if USE_CACHE then gameAssetsStr.Add(s + '=' + v);
        Result := v;
    end;
end;

// Find and return an IwbMainRecord, found by type (signature) and editor ID.
function GetMainRecord(f: IwbFile; t: string; id: string): IwbMainRecord;
var
    s: string;
    i: integer;
    v: IwbElement;
begin
    s := GetFileName(f) + t + SEP + id;
    i := gameAssetsElem.IndexOf(s);
    if USE_CACHE and (i >= 0) then
        Result := ObjectToElement(gameAssetsElem.Objects[i])
    else begin
        Log(10, '*');
        v := MainRecordByEditorID(GroupBySignature(f, t), id);
        if USE_CACHE then gameAssetsElem.AddObject(s, v);
        Result := v;
    end;
end;

function GetGameAssetElem(f: IwbFile; t: string; id: string; p: string): IwbElement;
var
    s: string;
    i: integer;
    mr: IwbMainRecord;
    v: IwbElement;
begin
    inc(logIndent);
    s := GetFileName(f) + t + SEP + id + SEP + p;
    i := gameAssetsElem.IndexOf(s);
    if USE_CACHE and (i >= 0) then
        Result := ObjectToElement(gameAssetsElem.Objects[i])
    else begin
        mr := GetMainRecord(f, t, id);
        Log(10, '*');
        v := ElementByPath(mr, p);
        if USE_CACHE then gameAssetsElem.AddObject(s, v);
        Result := v;
    end;
    dec(logIndent);
end;

function GetMRAssetStr(mr: IwbMainRecord; p: string): string;
var
    s: string;
    i: integer;
    v: string;
begin
    Log(6, '<GetMRAssetStr ' + EditorID(mr) + '/' + p);
    s := GetFileName(GetFile(mr)) + SEP + Signature(mr) + SEP + EditorID(mr) + SEP + p;
    i := gameAssetsStr.IndexOfName(s);
    if USE_CACHE and (i >= 0) then begin
        Log(6, 'Found value at ' + IntToStr(i));
        Result := gameAssetsStr.ValueFromIndex[i];
    end
    else begin
        Log(10, '*');
        v := GetElementEditValues(mr, p);
        Log(6, 'Adding new value: ' + s + '=' + v);
        if USE_CACHE then gameAssetsStr.Add(s + '=' + v);
        Result := v;
    end;
    Log(6, '>GetMRAssetStr');
end;

// Determine whether an element list (e.g. list of keywords, list of races) 
// containts the given element, with the element given by editor ID.
function ElementListContains(mr: IwbMainRecord; p: string; v: string): boolean;
var
    i: integer;
    e: IwbElement;
begin
    Log(11, '<ElementListContains ' + EditorID(mr) + '/' + p + '=' + v);
    Result := false;
    for i := 0 to HighInteger do begin
        e := ElementByIndex(ElementByPath(mr, p), i);
        if not Assigned(e) then break;
        Log(11, 'Found in list: ' + EditorID(LinksTo(e)));
        Result := SameText(EditorID(LinksTo(e)), v);
        if Result then break;
    end;
    Log(11, '>ElementListContains: ' + IfThen(Result, 'T', 'F'));
end;

//=======================================================================
// Assign the target element, which holds a reference to another
// main record, to the given record.
//
// This is monumentally stupid, but when there are ESLs screwing up the load
// order and when there are enough mods that the high bit ($80000000) is
// set, usual methods of handling form IDs don't work. So we construct
// the form ID as a string and set it as an Edit value.
//
procedure SetElementRef(theElement: IwbElement; val: IwbMainRecord);
var
    id: Cardinal;
    s: string;
begin
    id := GetLoadOrderFormID(val);
    s := IntToHex(id shr 24, 2) + IntToHex((id and $ffffff),6);
    SetEditValue(theElement, s);
end;

procedure SetElementRefStr(theElement: IwbElement; theFile: IwbFile; sig: string; val: string);
begin
    SetElementRef(theElement, GetMainRecord(theFile, sig, val));
end;

// <<<<<<<<<<<<<<<<<<<<< MANAGE TINT LAYERS >>>>>>>>>>>>>>>>>>>>>

procedure LoadTintLayer(r: integer; sex: integer; tli: integer; 
    mt: string; thisLayer: IwbElement);
begin
    inc(logIndent);
    Log(5, 'Loading tint layer ' + mt + ' race=' + IntToStr(r) +
        ' sex=' + IntToStr(sex) + ' tint=' + IntToStr(tli));
    raceInfo[r, sex].tints[tli].tini :=
        GetElementNativeValues(thisLayer, 'Tint Layer\Texture\TINI');
    raceInfo[r, sex].tints[tli].maskType := mt;
    raceInfo[r, sex].tints[tli].element := thisLayer;
    Log(5, 'Found tint mask ' + mt + ' TINI='
        + IntToStr(raceInfo[r, sex].tints[tli].tini));
        
    Log(5, 'Found ' + IntToStr(raceInfo[r, sex].tints[tli].presetsCount) +
        ' presets starting at ' + IntToStr(raceInfo[r, sex].tints[tli].presetsStart));
    dec(logIndent);
end;

procedure LoadTintsForSex(theRace: IwbMainRecord; sex: integer; tintlayers: IwbElement);
var
    i, r, tli: integer;
    thisLayer: IwbElement;
    mt: string;
    fn: string;
begin
    Log(5, '<LoadTintsForSex ' + Name(theRace) + ' ' + IfThen(sex=MALE, 'M', 'F'));
    r := masterRaceList.IndexOf(EditorID(theRace));
    raceINfo[r, sex].mainRecord := theRace;
    raceInfo[r, sex].muzzleCount := 0;
    raceInfo[r, sex].maskCount := 0;

    for i := 0 to ElementCount(tintlayers)-1 do begin
        thisLayer := ElementByIndex(tintlayers, i);
        mt := GetElementEditValues(thisLayer, 'Tint Layer\Texture\TINP');
        Log(5, 'Found tint layer ' + mt);

        tli := tintlayersList.IndexOf(mt);
        if tli >= 0 then LoadTintLayer(r, sex, tli, mt, thisLayer);

        fn := GetElementEditValues(thisLayer, 'Tint Layer\Texture\TINT');
        // Log(5, 'Found ' + fn + ': ' 
        //     + IfThen(EndsText('mask.dds', fn), 'MASK', ' ')
        //     + IfThen(EndsText('muzzle.dds', fn), 'MUZZLE', ''));

        if EndsText('old.dds', fn) then LoadTintLayer(r, sex, TL_OLD, 'Old', thisLayer);
        if EndsText('ear.dds', fn) or EndsText('ears.dds', fn) then
            LoadTintLayer(r, sex, TL_EAR, 'Ear', thisLayer);
        if EndsText('mask.dds', fn) then begin
            LoadTintLayer(r, sex, TL_MASK + raceInfo[r, sex].maskCount, 'Mask', thisLayer);
            raceInfo[r, sex].maskCount := raceInfo[r, sex].maskCount + 1;
        end;
        if EndsText('muzzle.dds', fn) then begin
            LoadTintLayer(r, sex, TL_MUZZLE + raceInfo[r, sex].muzzleCount, 'Muzzle', thisLayer);
            raceInfo[r, sex].muzzleCount := raceInfo[r, sex].muzzleCount + 1;
        end;

    end;
    Log(5, '>LoadTintsForSex');
end;

procedure CollectTintLayers(theRace: IwbMainRecord);
begin
    Log(2, '<LoadTintLayers ' + Name(theRace));

    tintlayersList := TStringList.Create;
    tintlayersList.Duplicates := dupIgnore;
    tintlayersList.Sorted := true;
    tintlayersList.Add('Cheek Color Lower');
    tintlayersList.Add('Cheek Color');
    tintlayersList.Add('Chin');
    tintlayersList.Add('Ear');
    tintlayersList.Add('EyeSocket Lower');
    tintlayersList.Add('EyeSocket Upper');
    tintlayersList.Add('Eyeliner');
    tintlayersList.Add('Forehead');
    tintlayersList.Add('Laugh Lines');
    tintlayersList.Add('Lip Color');
    tintlayersList.Add('Neck');
    tintlayersList.Add('Nose');
    tintlayersList.Add('Old');
    tintlayersList.Add('Skin Tone');
    tintlayersList.Add('Mask 0');
    tintlayersList.Add('Mask 1');
    tintlayersList.Add('Mask 2');
    tintlayersList.Add('Mask 3');
    tintlayersList.Add('Muzzle 0');
    tintlayersList.Add('Muzzle 1');
    tintlayersList.Add('Muzzle 2');
    tintlayersList.Add('Muzzle 3');
    tintlayersList.Add('Muzzle 4');
    tintlayersList.Add('Muzzle 5');
    tintlayersList.Add('Muzzle 6');
    tintlayersList.Add('Muzzle 7');
    TL_CHEEK_COLOR_LOWER := tintlayersList.IndexOf('Cheek Color Lower');
    TL_CHEEK_COLOR := tintlayersList.IndexOf('Cheek Color');
    TL_CHIN := tintlayersList.IndexOf('Chin');
    TL_EAR := tintlayersList.IndexOf('Ear');
    TL_EYESOCKET_LOWER := tintlayersList.IndexOf('EyeSocket Lower');
    TL_EYESOCKET_UPPER := tintlayersList.IndexOf('EyeSocket Upper');
    TL_EYELINER := tintlayersList.IndexOf('Eyeliner');
    TL_FOREHEAD := tintlayersList.IndexOf('Forehead');
    TL_LAUGH_LINES := tintlayersList.IndexOf('Laugh Lines');
    TL_LIP_COLOR := tintlayersList.IndexOf('Lip Color');
    TL_NECK := tintlayersList.IndexOf('Neck');
    TL_NOSE := tintlayersList.IndexOf('Nose');
    TL_OLD := tintlayersList.IndexOf('Old');
    TL_SKIN_TONE := tintlayersList.IndexOf('Skin Tone');
    TL_MASK := tintlayersList.IndexOf('Mask 0');
    TL_MUZZLE := tintlayersList.IndexOf('Muzzle 0');
    
    // Because we can't use the const or dynamic arrays, check for errors
    if tintlayersList.count <> TINTLAYERS_COUNT then Err('Tint layer count incorrect');
    if Length(raceInfo[0, 0].tints) <> TINTLAYERS_COUNT then Err('Tint layer array wrong size');

    LoadTintsForSex(theRace, MALE, ElementByPath(theRace, 'Head Data\Male Head Data\Tint Masks'));
    LoadTintsForSex(theRace, FEMALE, ElementByPath(theRace, 'Head Data\Female Head Data\Tint Masks'));

    Log(2, '>LoadTintLayers ');
end;

//=========================================================================
// Given a tint index from a NPC record, find the corresponding tint mask in the race record
// return the TINI index; -1 if not found
// returns found mask type in foundLayerMaskType
//
function FindRaceTintLayerByTINI(r: integer; sex: integer; targetTINI: integer): integer;
var
	skinStr: string;
	i, rli: integer;
	raceTINI: integer;
	layer: IInterface;
	layerFile, layerName: string;
	maskList, mask: IInterface;
	found: integer;
begin
	Log(5, '<FindRaceTintLayerByTINI: ' + IntToStr(targetTINI) + ' in ' + EditorID(raceInfo[r, sex].mainRecord) +
        IfThen(sex=0, ' M', ' F'));

    // Walk the race's tint layers until we find the one requested
    found := -1;
    for i := 0 to TINTLAYERS_COUNT-1 do begin
        Log(6, 'Checking tint ' + IntToStr(raceInfo[r, sex].tints[i].TINI));

        if raceInfo[r, sex].tints[i].TINI = targetTINI then
            found := i;
        if found >= 0 then break;
    end;

    Result := found;

    Log(5, '>FindRaceTintLayerByTINI ' + IntToStr(found));
end;

//===================================================
// Pick out a color from the presets by name of color
// Returns the preset itself
Function ChoosePresetByColor(raceIndex, sex: integer; colorName: string; tintLayer: integer): 
    IwbElement;
var
    presetlist: IwbElement;
	colorPreset: IwbElement;
	color: IwbMainRecord;
	i: integer;
begin
	Log(5, '<ChoosePresetByColor: ' +colorName);

    Result := nil;
    presetlist := ElementByPath(raceInfo[raceIndex, sex].tints[tintLayer].element, 'Presets');
    for i := 0 to ElementCount(presetlist) - 1 do begin
        colorPreset := ElementByIndex(presetlist, i);
        color := WinningOverride(LinksTo(ElementByPath(colorPreset, 'TINC')));
        if EditorID(color) = colorName then begin
            Result := colorPreset;
            break;
        end;
    end;

    Log(5, '>ChoosePresetByColor: ' + PathName(Result));

	// raceBaseLayer := ElementByIndex(theNPCRaceTintMasks, 0);
	// raceBasePresets := ElementByPath(raceBaseLayer, 'Presets');
	// for i := 0 to ElementCount(raceBasePresets)-1 do begin
	// 	colorPreset := ElementByIndex(raceBasePresets, i);
	// 	color := LinksTo(ElementByPath(colorPreset, 'TINC'));
	// 	name := EditorID(color);
	// 	if Pos(colorName, name) > 0 then break;
	// end;
	// Result := color;
end;

//===================================================
// Pick out a color from the presets by name of color
// Returns the color form
Function ChooseNamedColor(raceIndex, sex: integer; colorName: string; tintLayer: integer): IwbMainRecord;
var
	colorPreset: IwbElement;
	color: IwbMainRecord;
	i: integer;
begin
	Log(5, 'ChooseNamedColor:  ' +colorName);
    Result := WinningOverride(LinksTo(
        ElementByPath(ChoosePresetByColor(raceIndex, sex, colorName, tintLayer),
                      'TINC')));

	// raceBaseLayer := ElementByIndex(theNPCRaceTintMasks, 0);
	// raceBasePresets := ElementByPath(raceBaseLayer, 'Presets');
	// for i := 0 to ElementCount(raceBasePresets)-1 do begin
	// 	colorPreset := ElementByIndex(raceBasePresets, i);
	// 	color := LinksTo(ElementByPath(colorPreset, 'TINC'));
	// 	name := EditorID(color);
	// 	if Pos(colorName, name) > 0 then break;
	// end;
	// Result := color;
end;

//======================================================
// Find a tint layer by mask filename
// No magic caching, might be slow. But used only for a few NPCs.
// Returns index into the tint list
function FindTintLayerByFilename(raceIndex, sex: integer; filename: string): integer;
var
    i: integer;
    n: string;
begin
    Log(9, '<FindTintLayerByFilename: ' + filename);
    Result := -1;

    for i := 0 to TINTLAYERS_COUNT-1 do begin
        if Assigned(raceInfo[raceIndex, sex].tints[i].element) then begin
            n := GetElementEditValues(raceInfo[raceIndex, sex].tints[i].element, 'Tint Layer\Texture\TINT');
            Log(9, 'Checking ' + PathName(raceInfo[raceIndex, sex].tints[i].element));
            Log(9, 'Checking ' + n);
            if ContainsText(n, filename) then begin
                Result := i;
                break;
            end;
        end;
    end;
    Log(9, '>FindTintLayerByFilename: ' + IntToStr(Result));
end;

function GetTINIByTintIndex(raceIndex, sex, theTintIndex: integer): integer;
begin   
    Result := raceInfo[raceIndex, sex].tints[theTintIndex].TINI;
end;

// <<<<<<<<<<<<<<<<<<<<  MANAGE HEAD PARTS  >>>>>>>>>>>>>>>>>>>>

function HeadpartFacialType(hp: IwbMainRecord): integer;
// Return the facial type of the head part as an index into headpartsList
var
    s: string;
    i: integer;
begin
    s := GetElementEditValues(hp, 'PNAM');
    i := headpartsList.IndexOf(s);
    // Log(5, 'HeadpartFacialType of ' + Name(hp) + ' = ' + IntToStr(i));
    Result := i;
end;

function HeadpartSexIs(hp: IwbMainRecord; sex: integer): boolean;
// Determine whether the head part hp works for the given sex
begin
    // Log(5, 'HeadpartSexIs M:' + GetMRAssetStr(hp, 'DATA - Flags\Male')
    //     + 'F:' + GetMRAssetStr(hp, 'DATA - Flags\Female'));
    Result := ((sex = MALE) and (GetElementEditValues(hp, 'DATA - Flags\Female') = '0'))
        or
        ((sex = FEMALE) and (GetElementEditValues(hp, 'DATA - Flags\Male') = '0'));
end;

procedure LoadHeadPart(hp: IwbMainRecord; targetHeadparts: TStringList);
// Load the head part and add it to the listed races' head parts
var 
    i: integer;
    raceRef: IwbElement;
    raceRec: IwbMainElement;
    raceName: string;
    raceIndex: integer;
    validRaceList: IwbMainRecord;
    hpsex, sex: integer;
    facialType: integer;
begin
    Log(4, '<LoadHeadPart ' + FormName(hp) + ' '  + GetElementEditValues(hp, 'PNAM'));

    if targetHeadparts.IndexOf(EditorID(hp)) >= 0 then 
        specialHeadparts.AddObject(EditorID(hp), hp);

    // Get the form list that has the races for this head part
    Log(5, 'Found reference to form list ' 
        + EditorID(LinksTo(ElementByPath(hp, 'RNAM - Valid Races'))));
    validRaceList := WinningOverride(LinksTo(ElementByPath(hp, 'RNAM - Valid Races')));
    facialType := HeadpartFacialType(hp);

    if facialType < 0 then 
        Err('Unknown facial type: ' GetElementEditValues(hp, 'PNAM'))
    else begin
        Log(5, 'Headpart is for [' 
            + IfThen(HeadpartSexIs(hp, MALE), 'M', '') + ' '
            + IfThen(HeadpartSexIs(hp, FEMALE), 'F', '') + ']');
        
        for i := 0 to HighInteger do begin
            raceRef := ElementByIndex(
                ElementByPath(validRaceList, 'FormIDs'), i);
            if not Assigned(raceRef) then break;
            raceRec := LinksTo(raceRef);
            raceName := EditorID(raceRec);
            Log(5, 'Found reference to race ' + FormName(raceRec));
            raceIndex := masterRaceList.IndexOf(racename);
            if raceIndex >= 0 then begin
                for sex := MALE to FEMALE do begin
                    if HeadpartSexIs(hp, sex) then begin
                        Log(4, 'Race has new headpart: ' +
                            inttostr(raceIndex) + ', ' +
                            inttostr(sex) + ', ' + 
                            inttostr(facialType));
                        if not Assigned(raceInfo[raceIndex, sex].headparts[facialType]) then 
                            raceInfo[raceIndex, sex].headParts[facialType] := TStringList.Create;
                        raceInfo[raceIndex, sex].headParts[facialType]
                            .AddObject(EditorId(hp), hp);
                        Log(4, 'Race ' + racename + ' has HP ' + EditorID(hp));
                    end;
                end;
            end;
        end;
    end;

    Log(4, '>LoadHeadPart');
end;

procedure CollectRaceHeadparts(targetHeadparts: TStringList);
var
    hpDone: TStringList;
    i, j: integer;
    g: IwbContainer;
    f: IwbFile;
    hp: IwbMainRecord;
    hpname: string;
begin
    Log(1, '<CollectRaceHeadparts');
    
    hpDone := TStringList.Create;
    hpDone.Duplicates := dupIgnore;
    hpDone.Sorted := true;

	for i := 0 to Pred(FileCount()) do begin
		f := FileByLoadOrder(i);

        g := GroupBySignature(f, 'HDPT');
        for j := 0 to Pred(ElementCount(g)) do begin
            hp := WinningOverride(ElementByIndex(g, j));
            hpname := EditorID(hp);

            if hpDone.IndexOf(hpname) < 0 then LoadHeadpart(hp, targetHeadparts);
            hpDone.Add(hpname);

            if hpDone.Count > LIMIT then break;
        end;
        if SameText(GetFileName(f), yaFileName) then break; // Stop when we hit YA
    end;

    hpDone.Free;
    Log(1, '>CollectRaceHeadparts');
end;

// <<<<<<<<<<<<<<<<<<<<<< MANAGE RACES >>>>>>>>>>>>>>>>>>>>>>

procedure CollectRaces;
var
    racesFound: TStringList;
    i, j: integer;
    g: IwbContainer;
    f: IwbFile;
    race: IwbMainRecord;
    racename: string;
begin
    Log(1, '<CollectRaces');

    racesFound := TStringList.Create;
    racesFound.Sorted := true;
    racesFound.Duplicates := dupIgnore;

    masterRaceList := TStringList.Create;
    masterRaceList.Sorted := false;
    masterRaceList.Duplicates := dupIgnore;

	for i := 0 to Pred(FileCount()) do begin
		f := FileByLoadOrder(i);

        g := GroupBySignature(f, 'RACE');
        for j := 0 to Pred(ElementCount(g)) do begin
            race := WinningOverride(ElementByIndex(g, j));
            racename := EditorID(race);
            log(14, 'Found race ' + racename + ': ' 
                + IntToStr(racesFound.IndexOf(racename)));

            if (racesFound.IndexOf(racename) < 0) then begin
                if (masterRaceList.count < MAX_RACES) then begin 
                    //Log(3, 'Checking race ' + FormName(race));
                    if ElementListContains(race, 'KWDA', 'ActorTypeNPC') then begin
                        //Log(3, 'Adding NPC race ' + Name(race));
                        masterRaceList.AddObject(racename, race);
                        CollectTintLayers(race);
                    end;
                end
            end;
            if racesFound.IndexOf(racename) < 0 then begin
                Log(14, 'Adding race to found races ' + racename + ': ' 
                    + IntToStr(racesFound.count+1));
                racesFound.add(racename);
            end
                else Log(14, 'Race already in found races: ' + racename + ': ' 
                    + IntToStr(racesFound.IndexOf(racename)));
        end;
    end;
    racesFound.Free;

    if masterRaceList.count >= MAX_RACES-1 then 
        Err('race count exceeds max: ' + inttostr(masterRaceList.count));
    
    Log(1, '>CollectRaces');
end;


// <<<<<<<<<<<<<<<<<Access functions to hide the implementation>>>>>>>>>>>>>>>>>

function GetRaceTintTINI(theRace, sex, tintLayer: integer): integer;
begin
    Result := raceInfo[theRace, sex].tints[tintLayer].TINI;
end;

function GetRaceTintMaskType(theRace, sex, tintLayer: integer): string;
begin
    Log(6, 'GetRaceTintMaskType: tintLayer=' + IntToStr(tintLayer));
    Result := raceInfo[theRace, sex].tints[tintLayer].maskType;
end;

function GetRaceMaskCount(theRace, sex: integer): integer;
begin
    Result := raceInfo[theRace, sex].maskCount;
end;

function GetRaceMuzzleCount(theRace, sex: integer): integer;
begin
    Result := raceInfo[theRace, sex].muzzleCount;
end;

function GetRaceTintElement(theRace, sex, tintLayer: integer): IwbElement;
begin
    Result := raceInfo[theRace].tints[sex, tintLayer].element;
end;

// Pick a random preset from a tint layer
// Return the preset element itself.
// ind = 1 to skip the initial preset, which is often "no tint"
Function PickRandomTintPreset(seed: string; hv, theRace, sex, tintLayer, ind: integer): IwbElement;
var
    r: integer;
    p: IwbContainer;
Begin
    p := ElementByPath(raceInfo[theRace, sex].tints[tintLayer].element, 'Presets');
    if ElementCount(p) = 0 then
        Result := nil
    else begin
        r := HashInt(seed, hv, ind, ElementCount(p)-1);
        Result := ElementByIndex(p, r);
    end;
end;

Function GetRaceHeadpartCount(theRace, sex, headpart: integer): integer;
begin
    if headpart >= HEADPART_TYPES_COUNT then begin
        Err('GetRaceHeadpartCount: headpart type index too large: ' + IntToStr(headpart));
        Result := 0;
    end
    else if not Assigned(raceInfo[theRace, sex].headparts[headpart]) then
        Result := 0
    else
        Result := raceInfo[theRace, sex].headparts[headpart].Count;
end;

function GetRaceHeadpart(theRace, sex, headpart, hpIndex: integer): IwbMainRecord;
begin
    Result := ObjectToElement(
        raceInfo[theRace, sex].headparts[headpart].Objects[hpIndex]);
end;

function HeadpartValidForRace(theHP: IwbMainRecord; raceIndex, sex, hpType: integer): boolean;
begin
    Result := raceInfo[raceIndex, sex].headparts[hpType].IndexOf(EditorID(theHP)) >= 0;
end;

// <<<<<<<<<<<<<<<<<<<<<<<<<<<< DEBUGGING >>>>>>>>>>>>>>>>>>>>>>>>>>>>

procedure Test1;
var
    nr: IwbMainRecord;
    hp: IwbElement;
    sl: TStringList;
    sl2: TStringList;
    slx: TStringList;
    i: integer;
    f: IwbFile;
    tp, tp2: TTintPreset;
begin
    for i := 0 to FileCount()-1 do begin    
        f := FileByLoadOrder(i);
        if GetFileName(f) = 'Skyrim.esm' then skyrimFile := f
        else if GetFileName(f) = 'YiffyAgeConsolidated.esp' then yaFile := f;
    end;

    nr := GetMainRecord(yaFile, 'RACE', 'NordRace');
    sl := TStringList.Create;
    sl.AddObject('a', nr);
    sl.AddObject('b', nr);
    sl2 := TStringList.Create;
    sl2.AddObject('obj', sl);
    AddMessage('sl2 size=' + IntToStr(sl2.count));
    AddMessage('sl2 0 size=' + IntToStr(sl2.Objects[0].Count));
    slx := sl2.Objects[0];
    slx.AddObject('xxx', nr);
    AddMessage('sl2 0 idx=' + IntToStr(slx.IndexOf('b')));
    AddMessage('sl2 0 val=' + EditorID(ObjectToElement(slx.Objects[2])));
    AddMessage('-----');

    AddMessage('Nord male skeleton = ' + GetGameAssetStr(skyrimFile, 'RACE', 'NordRace', 'ANAM - Male Skeletal Model'));
    AddMessage('Nord male skeleton YA = ' + GetGameAssetStr(yaFile, 'RACE', 'NordRace', 'ANAM - Male Skeletal Model'));
    AddMessage('Nord male skeleton YA = ' + GetGameAssetStr(yaFile, 'RACE', 'NordRace', 'ANAM - Male Skeletal Model'));
    AddMessage('Nord male skeleton YA = ' + GetGameAssetStr(yaFile, 'RACE', 'NordRace', 'ANAM - Male Skeletal Model'));
    AddMessage('Nord keyword count = ' + IntToStr(ElementCount(GetGameAssetElem(yaFile, 'RACE', 'NordRace', 'KWDA'))));
    AddMessage('High elf race = ' + EditorID(GetMainRecord(yaFile, 'RACE', 'HighElfRace')));
    AddMessage('High elf race = ' + EditorID(GetMainRecord(yaFile, 'RACE', 'HighElfRace')));

    AddMessage('Default face texture = ' + GetElementEditValues(nr, 'Head Data\Male Head Data\DFTM'));
    AddMessage('Default face texture = ' + GetElementEditValues(nr, 'Head Data\Male Head Data\DFTM'));
    AddMessage('Default face texture = ' + GetElementEditValues(nr, 'Head Data\Male Head Data\DFTM'));

    AddMessage('First actor effect = ' + EditorID(LinksTo(
        ElementByIndex(ElementByPath(nr, 'Actor Effects'), 0))));
    AddMessage('Second actor effect = ' + EditorID(LinksTo(
        ElementByIndex(ElementByPath(nr, 'Actor Effects'), 1))));

    hp := ElementByIndex(ElementByPath(
            nr, 'Head Data\Male Head Data\Head Parts'), 0);
    AddMessage('First head part = ' + EditorID(LinksTo(ElementByPath(hp, 'HEAD'))));
    AddMessage('First head part index = ' + GetEditValue(ElementByPath(hp, 'INDX')));

    sl.Free;
    sl2.Free;
end;

procedure TestTintLayers;
var
    myPresetList: TList;
begin
    // AddMessage('Setting up tint layer array');
    // myPresetList := TList.create;
    // myPresetList.Add()
    // SetLength(myPresetList, 5);
    // myPresetList[4].presetIndex := 5555;
    // myPresetList[0].presetIndex := 1234;
    // AddMessage('Have preset index ' + IntToStr(myPresetList[0].presetIndex));

    // racetintLayers[0].mainRecord := GetMainRecord(yaFile, 'RACE', 'NordRace');
    // racetintLayers[0].skinTone
end;

// Debugging routine - what did we find?
procedure ListValidHeadparts;
var
    i, j, k, l: integer;
begin
    for i := 0 to masterRaceList.count-1 do begin
        AddMessage('Race ' + masterRaceList.Strings[i]);
        for j := MALE to FEMALE do begin
            AddMessage('|   ' + IfThen(j = MALE, 'Male', 'Female'));
            for k := 0 to headpartsList.count-1 do begin
                AddMessage('|   |   ' + headpartsList.Strings[k]);
                if Assigned(raceInfo[i, j].headParts[k]) then 
                    for l := 0 to raceInfo[i, j].headParts[k].count-1 do begin
                        AddMessage('|   |   |   ' + 
                            Name(ObjectToElement(raceInfo[i, j].headParts[k]
                                .Objects[l])));
                    end;
            end;
        end;
    end;
end;

procedure ListTintLayers;
var i, j, k, p: integer;
begin
    AddMessage('--------- Tint Layers ----------');
    for i := 0 to masterRaceList.count-1 do begin
        AddMessage('Race ' + IntToStr(i) + ': '
            + Name(ObjectToElement(masterRaceList.Objects[i])));
        for j := MALE to FEMALE do begin
            AddMessage('|   ' + IfThen(j = MALE, 'Male', 'Female'));
            for k := 0 to TINTLAYERS_COUNT-1 do begin
                AddMessage('|   |   ' 
                + tintlayersList[k] + ' = '
                + IntToStr(raceInfo[i, j].tints[k].tini));
            end;
            for k := 0 to raceInfo[i, j].maskCount-1 do begin
                AddMessage('|   |   ' 
                + raceInfo[i, j].tints[TL_MASK + k].maskType + ' = '
                + IntToStr(raceInfo[i, j].tints[TL_MASK + k].tini));
                for p := 0 to raceInfo[i, j].tints[TL_MASK + k].presetsCount-1 do
                    AddMessage('|   |   '
                        + PathName(allPresets[raceInfo[i, j].tints[TL_MASK + k].presetsStart+p]));
            end;
            for k := 0 to raceInfo[i, j].muzzleCount-1 do begin
                AddMessage('|   |   ' 
                + raceInfo[i, j].tints[TL_MUZZLE + k].maskType + ' = '
                + IntToStr(raceInfo[i, j].tints[TL_MUZZLE + k].tini));
                for p := 0 to raceInfo[i, j].tints[TL_MUZZLE + k].presetsCount-1 do
                    AddMessage('|   |   '
                        + PathName(allPresets[raceInfo[i, j].tints[TL_MUZZLE + k].presetsStart+p]));
            end;
        end;
    end;
end;

procedure InitializeAssetLoader;
begin
    gameAssetsStr := TStringList.Create;
    gameAssetsStr.Duplicates := dupIgnore;
    gameAssetsStr.Sorted := true;
    gameAssetsElem := TStringList.Create;
    gameAssetsElem.Duplicates := dupIgnore;
    gameAssetsElem.Sorted := true;

    headpartsList := TStringList.Create;
    headpartsList.Duplicates := dupIgnore;
    headpartsList.Sorted := true;
    headpartsList.Add('Eyebrows');
    headpartsList.Add('Eyes');
    headpartsList.Add('Face');
    headpartsList.Add('Facial Hair');
    headpartsList.Add('Hair');
    headpartsList.Add('Misc');
    headpartsList.Add('Scar');
    HEADPART_EYEBROWS := headpartsList.IndexOf('Eyebrows');
    HEADPART_EYES := headpartsList.IndexOf('Eyes');
    HEADPART_FACE := headpartsList.IndexOf('Face');
    HEADPART_FACIAL_HAIR := headpartsList.IndexOf('Facial Hair');
    HEADPART_HAIR := headpartsList.IndexOf('Hair');
    HEADPART_MISC := headpartsList.IndexOf('Misc');
    HEADPART_SCAR := headpartsList.IndexOf('Scar');

    specialHeadparts := TStringList.Create;
    specialHeadparts.Duplicates := dupIgnore;
    specialHeadparts.Sorted := true;
end;

procedure LoadRaceAssets(targetHeadparts: TStringList);
begin
    CollectRaces;
    CollectRaceHeadparts(targetHeadparts);
end;

procedure ShutdownAssetLoader;
var
    i, j, k: integer;
begin
    for i := 0 to masterRaceList.Count-1 do begin
        for j := MALE to FEMALE do begin
            for k := 0 to headpartsList.count - 1 do begin
                if Assigned(raceInfo[i, j].headParts[k]) then  
                    raceInfo[i, j].headParts[k].Free;
            end;
        end;
    end;

    gameAssetsElem.Free;
    gameAssetsStr.Free;
    headpartsList.Free;
    specialHeadparts.Free;
    tintlayersList.Free;
end;

function Finalize: integer;
var
    g: IwbContainer;
    f: IwbFile;
    race: IwbMainRecord;
    racename: string;
    racepnam: float;
    myHeadparts: TStringList;
begin
    errCount := 0;
    myHeadparts := TStringList.Create;
    myHeadparts.Duplicates := dupIgnore;
    myHeadparts.Sorted := true;

    AddMessage('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
    InitializeAssetLoader;
    LoadRaceAssets(myHeadparts);

    //TestTintLayers;

    //Test1;
    ListValidHeadparts;
    ListTintLayers;

    AddMessage('Completed with ' + IntToStr(errCount) + ' errors');
    ShutdownAssetLoader;
    AddMessage('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
    myHeadparts.Free;
end;

end.