Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions AssetEditor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bmd", "Bmd", "{1D70E263-412
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Editors.BmdEditor", "Editors\BmdEditor\Editors.BmdEditor.csproj", "{9CFEEAF2-8260-F2F5-50FB-8FDA9BD12CF9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Csc", "Csc", "{A1C5C1E0-52D6-4F61-B8AE-0E0C64F3A9B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Editors.CscEditor", "Editors\CscEditor\Editors.CscEditor\Editors.CscEditor.csproj", "{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.CscEditor", "Editors\CscEditor\Test.CscEditor\Test.CscEditor.csproj", "{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CampaignAnimationCreator", "CampaignAnimationCreator", "{ED432A81-F5C7-0469-2ED7-3AF47D036060}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Editor.CampaignAnimationCreator", "Editors\CampaignAnimationCreator\Editor.CampaignAnimationCreator\Editor.CampaignAnimationCreator.csproj", "{438E6F1B-F5A9-49F3-8516-CB5E9D316C81}"
Expand Down Expand Up @@ -568,6 +574,30 @@ Global
{9CFEEAF2-8260-F2F5-50FB-8FDA9BD12CF9}.Release|x64.Build.0 = Release|Any CPU
{9CFEEAF2-8260-F2F5-50FB-8FDA9BD12CF9}.Release|x86.ActiveCfg = Release|Any CPU
{9CFEEAF2-8260-F2F5-50FB-8FDA9BD12CF9}.Release|x86.Build.0 = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|x64.ActiveCfg = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|x64.Build.0 = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|x86.ActiveCfg = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Debug|x86.Build.0 = Debug|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|Any CPU.Build.0 = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|x64.ActiveCfg = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|x64.Build.0 = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|x86.ActiveCfg = Release|Any CPU
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17}.Release|x86.Build.0 = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|x64.ActiveCfg = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|x64.Build.0 = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|x86.ActiveCfg = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Debug|x86.Build.0 = Debug|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|Any CPU.Build.0 = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|x64.ActiveCfg = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|x64.Build.0 = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|x86.ActiveCfg = Release|Any CPU
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388}.Release|x86.Build.0 = Release|Any CPU
{438E6F1B-F5A9-49F3-8516-CB5E9D316C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{438E6F1B-F5A9-49F3-8516-CB5E9D316C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{438E6F1B-F5A9-49F3-8516-CB5E9D316C81}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -658,6 +688,9 @@ Global
{BB9F2AC1-680E-4289-B212-88744460383E} = {D84249ED-B24C-F001-6275-6713CC04DE13}
{1D70E263-4123-4F00-A6AB-A42611732697} = {07AC615B-A8FC-4E1A-BDD5-BC11452429A0}
{9CFEEAF2-8260-F2F5-50FB-8FDA9BD12CF9} = {1D70E263-4123-4F00-A6AB-A42611732697}
{A1C5C1E0-52D6-4F61-B8AE-0E0C64F3A9B1} = {07AC615B-A8FC-4E1A-BDD5-BC11452429A0}
{D3E7A2B4-9C41-4E8F-A6D2-7B5E9F0C3A17} = {A1C5C1E0-52D6-4F61-B8AE-0E0C64F3A9B1}
{7F8AC4BE-11D2-4E60-93A1-C5B2E4F0A388} = {A1C5C1E0-52D6-4F61-B8AE-0E0C64F3A9B1}
{ED432A81-F5C7-0469-2ED7-3AF47D036060} = {07AC615B-A8FC-4E1A-BDD5-BC11452429A0}
{438E6F1B-F5A9-49F3-8516-CB5E9D316C81} = {ED432A81-F5C7-0469-2ED7-3AF47D036060}
{91617891-AD16-423F-B56F-AC0D2CE82B95} = {ED432A81-F5C7-0469-2ED7-3AF47D036060}
Expand Down
1 change: 1 addition & 0 deletions AssetEditor/AssetEditor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<ProjectReference Include="..\Editors\TextureEditor\Editors.TextureEditor.csproj" />
<ProjectReference Include="..\Editors\TwuiEditor\Editor.Twui\Editors.Twui.csproj" />
<ProjectReference Include="..\Editors\BmdEditor\Editors.BmdEditor.csproj" />
<ProjectReference Include="..\Editors\CscEditor\Editors.CscEditor\Editors.CscEditor.csproj" />
<ProjectReference Include="..\GameWorld\GameWorldCore\GameWorld.Core\GameWorld.Core.csproj" />
<ProjectReference Include="..\Shared\EmbeddedResources\Shared.EmbeddedResources.csproj" />
<ProjectReference Include="..\Shared\SharedUI\Shared.Ui\Shared.Ui.csproj" />
Expand Down
1 change: 1 addition & 0 deletions AssetEditor/Services/DependencyInjectionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public DependencyInjectionConfig(bool loadResources = true)
new Editors.Twui.DependencyInjectionContainer(),
new Editors.Ipc.DependencyInjectionContainer(),
new Editors.BmdEditor.DependencyInjectionContainer(),
new Editors.CscEditor.DependencyInjectionContainer(),

// Host application
new DependencyInjectionContainer(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.12" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Shared\GameFiles\Shared.GameFormats.csproj" />
<ProjectReference Include="..\..\..\Shared\SharedCore\Shared.Core\Shared.Core.csproj" />
Expand Down
182 changes: 182 additions & 0 deletions Editors/CscEditor/Editors.CscEditor/Data/CscChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Shared.GameFormats.Esf;

namespace Editors.CscEditor.Data
{
/// <summary>
/// One keyframe of a CSC parameter curve:
/// F32(x=time), F32(y=value), Ascii(mode), Coord2d(tangentIn), Coord2d(tangentOut), Ascii(mode)
/// on the wire. Mode vocabulary seen in real files: "constant", "linear", "bezier_c".
/// </summary>
public class CscKeyframe
{
public float Time { get; set; }
public float Value { get; set; }
public string ModeIn { get; set; } = "linear";
public Coord2d TangentIn { get; set; }
public Coord2d TangentOut { get; set; }
public string ModeOut { get; set; } = "linear";

public CscKeyframe Clone() => new()
{
Time = Time,
Value = Value,
ModeIn = ModeIn,
TangentIn = TangentIn,
TangentOut = TangentOut,
ModeOut = ModeOut,
};
}

/// <summary>
/// One channel of a parameter group. The header F32 is the channel's live value only when it
/// has zero keyframes; any keyframe overrides it entirely (in-game confirmed behaviour).
/// <see cref="HasModeTags"/> tracks which of the two wire encodings the channel came from -
/// the modern header form (F32, Ascii, Ascii) or the older bare-F32 form - so it round-trips
/// in the same shape it was read in.
/// </summary>
public class CscChannel
{
public float Header { get; set; }
public string ModeA { get; set; } = "constant";
public string ModeB { get; set; } = "constant";
public bool HasModeTags { get; set; } = true;
public List<CscKeyframe> Keyframes { get; } = [];

public float Evaluate(float time)
{
if (Keyframes.Count == 0)
return Header;
if (time <= Keyframes[0].Time)
return Keyframes[0].Value;
if (time >= Keyframes[^1].Time)
return Keyframes[^1].Value;

var i = 0;
while (i < Keyframes.Count - 1 && Keyframes[i + 1].Time < time)
i++;

var a = Keyframes[i];
var b = Keyframes[i + 1];
var span = b.Time - a.Time;
if (span <= 0)
return b.Value;

if (a.ModeOut.StartsWith("constant", StringComparison.OrdinalIgnoreCase))
return a.Value;
if (a.ModeOut.StartsWith("linear", StringComparison.OrdinalIgnoreCase))
return a.Value + (b.Value - a.Value) * ((time - a.Time) / span);

return EvaluateBezier(a, b, time);
}

/// <summary>
/// Cubic bezier segment with the keyframes' tangent handles as relative control-point
/// offsets: P0=(a.t,a.v), P1=P0+a.TangentOut, P2=P3+b.TangentIn, P3=(b.t,b.v). Control X
/// is clamped inside [a.t, b.t] so x(s) stays monotonic and the x->s inversion (bisection)
/// is well defined.
/// </summary>
static float EvaluateBezier(CscKeyframe a, CscKeyframe b, float time)
{
var x0 = a.Time;
var y0 = a.Value;
var x3 = b.Time;
var y3 = b.Value;
var x1 = Math.Clamp(x0 + a.TangentOut.X, x0, x3);
var y1 = y0 + a.TangentOut.Y;
var x2 = Math.Clamp(x3 + b.TangentIn.X, x0, x3);
var y2 = y3 + b.TangentIn.Y;

float BezierX(float s)
{
var u = 1 - s;
return u * u * u * x0 + 3 * u * u * s * x1 + 3 * u * s * s * x2 + s * s * s * x3;
}

float lo = 0, hi = 1;
for (var iter = 0; iter < 32; iter++)
{
var mid = (lo + hi) * 0.5f;
if (BezierX(mid) < time)
lo = mid;
else
hi = mid;
}

var t = (lo + hi) * 0.5f;
var v = 1 - t;
return v * v * v * y0 + 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t * y3;
}

/// <summary>The value shown/edited when treating the channel as static (no time context).</summary>
public float StaticValue => Keyframes.Count == 0 ? Header : Keyframes[0].Value;

/// <summary>Adds a delta to the channel everywhere - header and every keyframe - so an
/// animated channel keeps its motion but shifts its base value.</summary>
public void OffsetAll(float delta)
{
Header += delta;
foreach (var k in Keyframes)
k.Value += delta;
}

public void ScaleAll(float factor)
{
Header *= factor;
foreach (var k in Keyframes)
k.Value *= factor;
}

public void SetStatic(float value)
{
if (Keyframes.Count == 0)
Header = value;
else
OffsetAll(value - StaticValue);
}

public CscKeyframe AddKeyframe(float time, float value)
{
var key = new CscKeyframe { Time = time, Value = value, ModeIn = "linear", ModeOut = "linear" };
Keyframes.Add(key);
SortKeyframes();
return key;
}

public void SortKeyframes() => Keyframes.Sort((a, b) => a.Time.CompareTo(b.Time));

public CscChannel Clone()
{
var clone = new CscChannel { Header = Header, ModeA = ModeA, ModeB = ModeB, HasModeTags = HasModeTags };
clone.Keyframes.AddRange(Keyframes.Select(k => k.Clone()));
return clone;
}
}

/// <summary>
/// A parameter group: U32(marker) + one or more channels. The marker is load-bearing
/// (flipping it from 1 crashed the game's loader in testing) so it is preserved verbatim.
/// </summary>
public class CscChannelGroup
{
public uint Marker { get; set; } = 1;
public List<CscChannel> Channels { get; } = [];

public static CscChannelGroup CreateStatic(params float[] channelHeaders)
{
var group = new CscChannelGroup();
foreach (var header in channelHeaders)
group.Channels.Add(new CscChannel { Header = header });
return group;
}

public CscChannelGroup Clone()
{
var clone = new CscChannelGroup { Marker = Marker };
clone.Channels.AddRange(Channels.Select(c => c.Clone()));
return clone;
}
}
}
Loading
Loading