library SKSE_Elys_Uncapper;{$R *.res}uses Windows, SysUtils, IniFiles, Classes;type PluginHandle = Longword; SKSEInterface = packed record skseVersion: Longword; runtimeVersion: Longword; editorVersion: Longword; isEditor: Longword; QueryInterface: procedure(id: Longword); cdecl; GetPluginHandle: function: PluginHandle; cdecl; end; PluginInfo = packed record const kInfoVersion: Longword = 1; var infoVersion: Longword; name: PAnsiChar; version: Longword; end; TPerkByLevel = record Level: Word; Perks: Byte; end;const kPluginHandle_Invalid = $FFFFFFFF; SKYRIM_VER = $030A0000; *JC* These would have to be changed for a new version SKSE_VER = $01040020; PLUGIN_VER = $01050040; oPLAYER = Pointer($01570334); *JC* This looks to me to be the address of the player's character. Was this value obtained through decompilation of Skyrim and examination of the assembly code? AlterationID = 18; ArcheryID = 8; AlchemyID = 16; ConjurationID = 19; BlockID = 9; LightArmorID = 12; DestructionID = 20; HeavyArmorID = 11; LockpickingID = 14; EnchantingID = 23; OneHandedID = 6; PickpocketID = 13; IllusionID = 21; SmithingID = 10; SneakID = 15; RestorationID = 22; TwoHandedID = 7; SpeechID = 17; *JC* These numbers for the skills, are they also from the game data? Would they be different with Skyrim 1.4.21?var Enabled: Boolean = False; SkillEffectFormulasCap: Single; FixSneakOver100CheckCode: packed Array [0 .. 5] of Byte = ( $9E, // sahf $9B, // wait $DF, $E0, // fstsw ax $90, // nop $73 // jae ); SkillCaps: packed Array [6 .. 23] of Single; SkillMults: Array [6 .. 23] of Single; PerksPerLevel: Array of TPerkByLevel; LastValidedSkill: Longword; CurrentSkillCap : Single; CurrentSkillLevel : Single; pSkillCapPatch: Pointer; pSkillCapPatch2: Pointer; SLParam1, SLParam2 : Single; GetActorLevel: function(a1, a2: Integer; This: Pointer): Word; cdecl; CalculateExpForNextLevel: function( SkillLevel, SLParam1, SLParam2 : Single ): Single; cdecl; sub_650A70: function(a1, a2, a3 : Longint; a4, a5: PSingle): Byte; cdecl;function Overwrite(Address: Pointer; Data: Pointer; Size: Longword): Boolean;var OldFlag: Longword;begin Result := False; if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, @OldFlag) then Move(Data^, Address^, Size) else begin MessageBox(0, 'SKSE Elys Uncapper could not modify Skyrim in memory. PLEASE CLOSE THE GAME NOW TO AVOID POTENTIAL GAME INCONSISTANCIES OR CORRUPTION', 'Error', MB_ICONERROR); Exit; end; Result := True;end;function SetFF15Call(Address: Pointer; Target: Pointer): Boolean;var Jmp: packed record Code: Word; Target: Pointer; end;begin Jmp.Code := $15FF; *JC* This value might have to be updated Jmp.Target := Target; Result := Overwrite(Address, @Jmp, 6);end;Function ReplaceE8CallTarget(Caller: PByte; Target: Pointer): Boolean;var Address: Longword;begin Address := Longword(Target) - Longword(Caller) - 5; Result := Overwrite(Pointer(Caller + 1), @Address, 4);end;function hook_650A70(a1, a2, a3 : Longint; a4, a5: PSingle): Byte; cdecl;begin Result := sub_650A70(a1, a2, a3, a4, a5); SLParam1 := a4^; SLParam2 := a5^end;function IncreasePerkPool(NewValue: Byte): Pointer; stdcall; // thiscallconst PerkOffset = $6C9; // offset to update with each new Skyrim version *JC* And $6C9 as wellvar This: PByte; Level: Word; i: Integer; b: Integer;begin asm push ecx // Thiscall fix (Useless in this case but better safe than sorry) mov This, ecx end; Level := GetActorLevel(0, 0, Pointer(oPLAYER^)); // Pointer to Player b:= -1; for i := 0 to High(PerksPerLevel) do if Level < PerksPerLevel[i].Level then break else if Level = PerksPerLevel[i].Level then begin b := PerksPerLevel[i].Perks; break; end else b := PerksPerLevel[i].Perks; if b >= 0 then b := (This + PerkOffset)^ + b else b := NewValue; if (This + PerkOffset)^ < b then (This + PerkOffset)^ := b; Result := This; asm pop ecx // Thiscall fix (Useless in this case but better safe than sorry) end;end;function IncreaseSkillExp(f: Single): Pointer; stdcall; // thiscallvar This: PByte;begin asm push ecx // Thiscall fix (Useless in this case but better safe than sorry) mov This, ecx end; if (PSingle(This + 8)^ = 0) and (CurrentSkillLevel < SkillCaps[LastValidedSkill]) then begin PSingle(This + 4)^ := 0; PSingle(This + 8)^ := PSingle(This + 4)^ + CalculateExpForNextLevel(CurrentSkillLevel+1, SLParam1, SLParam2); end; PSingle(This + 4)^ := PSingle(This + 4)^ + f * SkillMults[LastValidedSkill]; Result := This; asm pop ecx // Thiscall fix (Useless in this case but better safe than sorry) end;end;function GetSkill(i: Longword): Longword; cdecl;begin Result := i + 6; LastValidedSkill := Result;end;function IsValidSkill(Skill: Longword): Longbool; cdecl;begin Result := (Skill >= 6) and (Skill < 24); LastValidedSkill := Skillend;procedure SkillCapPatch;begin CurrentSkillCap := SkillCaps[LastValidedSkill]; asm fld CurrentSkillCap end;end;procedure SkillCapPatch2;asm mov eax, [ebp-4] mov CurrentSkillLevel, eax jmp SkillCapPatchend;function InitOptions: Boolean;const INI_ERROR = 'Error: Skyrim_Elys_Uncapper.ini'; IS_INVALID_VALUE = 'http://forums.bethsoft.com/topic/1287595-relwip-skyrim-elys-uncapper/is not a valid value for'; MAX_LEVEL = 10000;var Filename: String; IniFile: TMemIniFile; Strings: TStringList; i, j: Integer; Level: Integer; Perks: Integer; PerksPerLevelIndex: Integer; function SafeReadInteger(const name: String; Default: Integer): Integer; var idx: Integer; begin idx := Strings.IndexOfName(Name); if idx < 0 then Result := Default else if TryStrToInt(Strings.ValueFromIndex[idx], Result) and (Result >= 0) and (Result <= MAX_LEVEL) then Exit; Result := Default; MessageBox(0, PChar(Strings.ValueFromIndex[idx] + IS_INVALID_VALUE + Name + '.'), INI_ERROR, MB_ICONERROR); end; function SafeReadFloat(const name: String; Default: Single): Single; var idx: Integer; begin idx := Strings.IndexOfName(Name); if idx < 0 then Result := Default else if TryStrToFloat(Strings.ValueFromIndex[idx], Result) and (Result >= 0) and (Result <= MAX_LEVEL) then Exit; Result := Default; MessageBox(0, PChar(Strings.ValueFromIndex[idx] + IS_INVALID_VALUE + Name + '.'), INI_ERROR, MB_ICONERROR); end; function SafeReadBool(const name: String; Default: Boolean): Boolean; var idx: Integer; begin idx := Strings.IndexOfName(Name); if idx < 0 then Result := Default else if not TryStrToBool(Strings.ValueFromIndex[idx], Result) then begin Result := Default; MessageBox(0, PChar(Strings.ValueFromIndex[idx] + IS_INVALID_VALUE + Name + '.'), INI_ERROR, MB_ICONERROR); Exit; end; end;begin Result := True; SetLength(Filename, MAX_PATH + 1); GetModuleFileNameW(HInstance, @Filename[1], MAX_PATH + 1); IniFile := TMemIniFile.Create(ExtractFilePath(Filename) + 'SKSE_Elys_Uncapper.ini'); PerksPerLevelIndex := 0; try Strings := TStringList.Create; try FormatSettings.DecimalSeparator := '.'; SetLength(TrueBoolStrs, 2); TrueBoolStrs[0] := 'True'; TrueBoolStrs[1] := '1'; SetLength(FalseBoolStrs, 2); FalseBoolStrs[0] := 'False'; FalseBoolStrs[1] := '0'; IniFile.ReadSectionValues('General', Strings); Enabled := SafeReadBool('bEnabled', False); SkillEffectFormulasCap := SafeReadInteger('iSkillEffectFormulasCap', 100); Strings.Clear; IniFile.ReadSectionValues('SkillCaps', Strings); SkillCaps[AlterationID] := SafeReadInteger('iAlteration', 100); SkillCaps[ArcheryID] := SafeReadInteger('iArchery', 100); SkillCaps[AlchemyID] := SafeReadInteger('iAlchemy', 100); SkillCaps[ConjurationID] := SafeReadInteger('iConjuration', 100); SkillCaps[BlockID] := SafeReadInteger('iBlock', 100); SkillCaps[LightArmorID] := SafeReadInteger('iLightArmor', 100); SkillCaps[DestructionID] := SafeReadInteger('iDestruction', 100); SkillCaps[HeavyArmorID] := SafeReadInteger('iHeavyArmor', 100); SkillCaps[LockpickingID] := SafeReadInteger('iLockpicking', 100); SkillCaps[EnchantingID] := SafeReadInteger('iEnchanting', 100); SkillCaps[OneHandedID] := SafeReadInteger('iOneHanded', 100); SkillCaps[PickpocketID] := SafeReadInteger('iPickpocket', 100); SkillCaps[IllusionID]:= SafeReadInteger('iIllusion', 100); SkillCaps[SmithingID] := SafeReadInteger('iSmithing', 100); SkillCaps[SneakID] := SafeReadInteger('iSneak', 100); SkillCaps[RestorationID]:= SafeReadInteger('iRestoration', 100); SkillCaps[TwoHandedID] := SafeReadInteger('iTwoHanded', 100); SkillCaps[SpeechID] := SafeReadInteger('iSpeech', 100); Strings.Clear; IniFile.ReadSectionValues('SkillExpGainMults', Strings); SkillMults[AlterationID] := SafeReadFloat('fAlteration', 1); SkillMults[ArcheryID] := SafeReadFloat('fArchery', 1); SkillMults[AlchemyID] := SafeReadFloat('fAlchemy', 1); SkillMults[ConjurationID] := SafeReadFloat('fConjuration', 1); SkillMults[BlockID] := SafeReadFloat('fBlock', 1); SkillMults[LightArmorID] := SafeReadFloat('fLightArmor', 1); SkillMults[DestructionID] := SafeReadFloat('fDestruction', 1); SkillMults[HeavyArmorID] := SafeReadFloat('fHeavyArmor', 1); SkillMults[LockpickingID] := SafeReadFloat('fLockpicking', 1); SkillMults[EnchantingID] := SafeReadFloat('fEnchanting', 1); SkillMults[OneHandedID] := SafeReadFloat('fOneHanded', 1); SkillMults[PickpocketID] := SafeReadFloat('fPickpocket', 1); SkillMults[IllusionID] := SafeReadFloat('fIllusion', 1); SkillMults[SmithingID] := SafeReadFloat('fSmithing', 1); SkillMults[SneakID] := SafeReadFloat('fSneak', 1); SkillMults[RestorationID] := SafeReadFloat('fRestoration', 1); SkillMults[TwoHandedID] := SafeReadFloat('fTwoHanded', 1); SkillMults[SpeechID] := SafeReadFloat('fSpeech', 1); *JC* These lines don't need to be changed since they just read the ini. Don't want users of the new version to be unable to use their old settings, now do we? ~ Strings.Clear; IniFile.ReadSectionValues('PerksPerLevelAndAbove', Strings); SetLength(PerksPerLevel, Strings.Count); For i := 0 to Strings.Count - 1 do if TryStrToInt(Strings.Names[i], Level) and (Level > 0) and (Level <= MAX_LEVEL) then if TryStrToInt(Strings.ValueFromIndex[i], Perks) and (Perks >= 0) and (Perks <= 255) then begin PerksPerLevel[PerksPerLevelIndex].Level := Level; PerksPerLevel[PerksPerLevelIndex].Perks := Perks; Inc(PerksPerLevelIndex); end else MessageBox(0, PChar(Strings[i] + ' is not a correct Perks value for [PerksPerLevelAndAbove].'), INI_ERROR, MB_ICONERROR) else MessageBox(0, PChar(Strings[i] + ' is not a correct Level value for [PerksPerLevelAndAbove].'), INI_ERROR, MB_ICONERROR); finally Strings.Free; end; SetLength(PerksPerLevel, PerksPerLevelIndex); for j := High(PerksPerLevel) - 1 downto 1 do for i := 0 to j do if PerksPerLevel[i].Level > PerksPerLevel[i + 1].Level then begin Level := PerksPerLevel[i].Level; Perks := PerksPerLevel[i].Perks; PerksPerLevel[i].Level := PerksPerLevel[i + 1].Level; PerksPerLevel[i].Perks := PerksPerLevel[i + 1].Perks; PerksPerLevel[i + 1].Level := Level; PerksPerLevel[i + 1].Perks := Perks; end; except MessageBox(0, 'SKSE Elys Uncapper encountered an error while attempting to read SKSE_Elys_Uncapper.ini. The plugin is not enabled', 'Error', MB_ICONERROR); Result := False; end; IniFile.Free;end;Function SKSEPlugin_Query(const skse: SKSEInterface; var Info: PluginInfo): Boolean; cdecl;begin Info.infoVersion := PluginInfo.kInfoVersion; Info.name := 'SKSE_Elys_Uncapper'; Info.version := PLUGIN_VER; Result := False; if skse.isEditor <> 0 then Exit; if skse.skseVersion < SKSE_VER then begin MessageBox(0, 'SKSE Elys Uncapper requires SKSE Version 1.4.2 or higher.', 'Error: Plugin not enabled', MB_ICONERROR); Exit; end; if skse.runtimeVersion <> SKYRIM_VER then begin MessageBox(0, 'SKSE Elys Uncapper only supports Skyrim Version 1.3.10.', 'Error: Plugin not enabled', MB_ICONERROR); Exit; end; *JC* Suggestion: Instead of this line, let the plug in run. For each value that needs to be fetched from the game executable in memory, if the fetch succeeds, that value does not have to be changed in a new version of this Uncapper. If it fails, well, create a log where each entry is either "PerkOffset - $6C9 - FAILED" or "oPlayer - $01570334 - SUCCEEDED" I'll go into more detail later. Result := True;end;Function SKSEPlugin_Load(const skse: SKSEInterface): Boolean; cdecl;begin Result := False; If not InitOptions then Exit; if not Enabled then begin Result := True; Exit; end; GetActorLevel := Pointer($00A512D0); // Don't forget to update the call used with new Player pointer CalculateExpForNextLevel := Pointer($0088DB20); sub_650A70 := Pointer($00650A70); if not ReplaceE8CallTarget(PByte($0088DCE3), @IsValidSkill) then // IsValidSkill Exit; if not ReplaceE8CallTarget(PByte($0088DA62), @GetSkill) then // GetSKill Exit; pSkillCapPatch := @SkillCapPatch; pSkillCapPatch2 := @SkillCapPatch2; if not SetFF15Call(PByte(@CalculateExpForNextLevel)+9, @pSkillCapPatch) then // Skill Exp Curve Cap Exit; if not SetFF15Call(Pointer($0088DE80), @pSkillCapPatch) then // Skill Cap Exit; if not SetFF15Call(Pointer($0088DD27), @pSkillCapPatch2) then // Skill Cap 2 Exit; if not ReplaceE8CallTarget(PByte($0088DD6A), @hook_650A70) then // sub_650A70 hook; Exit; if not ReplaceE8CallTarget(PByte($009EA32D), @IncreasePerkPool) then // Perk Management. Dont forget update offset inside function Exit; if not ReplaceE8CallTarget(PByte($0088DE74), @IncreaseSkillExp) then // Skill Exp Mult Exit; if not Overwrite(Pointer($006537A8), @SkillEffectFormulasCap, 4) then // Uncap formulas Exit; if not Overwrite(Pointer($007D2024), @FixSneakOver100CheckCode, 6) then // Sneak Formula Cap Fix *JC* All of the $00xxxxxx values would have to be updated. Exit; Result := True;end;exports SKSEPlugin_Query, SKSEPlugin_Load;beginend.