namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
// Original 6891 image: 3B26700/3C415F0 (colour), 3C3F5A0/3C3F440
// (luminosity/subclass palette), 3B28090/3B3E640 (size). These methods
// copy only already loaded records. In particular, the original tree
// operator 3C284E0 INSERTS a missing key, so it is never invoked here.
internal readonly record struct GalaxyAppearance(uint typeRgba,uint? realisticRgba,float? colourBlend,
uint rawScale,ushort classification,float size,float smallSize,bool? smallSizeActive)
{
internal bool? navRouteMembership {get;init;}
internal int? navRouteMembershipMode {get;init;}
internal int? navRouteViewMode {get;init;}
}
readonly record struct GalaxyAppearanceSource(nuint registry,nuint expectedImage,ulong tick,float? blend,bool? small);
static GalaxyAppearanceSource galaxyAppearanceSource;
static GalaxyAppearanceSource GalaxyAppearanceSettings(GalaxyServices services)
{
var now=GetTickCount64();var cached=galaxyAppearanceSource;
if(cached.registry==services.registry&&cached.expectedImage==image&&now-cached.tick<100)return cached;
float? blend=null;bool? small=null;
try
{
// 3A06740: base+58 main interface; base+60 type interface.
// 3A55CF0 (main vtable+D8) returns float(main+99C).
var component=RegisteredComponent(services.registry,Read<uint>(image+0x5FA0A10));
if(component>=0x10060&&Pointer(component)==image+0x551F518&&Pointer(component-8)==image+0x551F3D8&&
Pointer(component-0x60)==image+0x551F2A8&&Pointer(component-0x58)==image+0x551F2D8)
{
var value=Read<float>(component-8+0x99C);
if(float.IsFinite(value)&&value>=0&&value<=1)blend=value;
}
// 3B3E640 calls the same settings interface+158. The live
// implementation 33D8280 reads byte(iface+624), while the
// alternate 552AE90 implementation returns constant true.
component=RegisteredComponent(services.registry,Read<uint>(image+0x5F05DD4));
if(component>=0x10000)
{
var table=Pointer(component);
if(table==image+0x552A9B8&&Pointer(table+0x158)==image+0x33D8220)
{
var value=Read<byte>(component+0x624);if(value<=1)small=value!=0;
}
else if(table==image+0x552AE60&&Pointer(table+0x158)==image+0x201130)small=true;
}
}
catch{}
var result=new GalaxyAppearanceSource(services.registry,image,now,blend,small);
galaxyAppearanceSource=result;return result;
}
static bool GalaxyAppearanceTree(nuint tree,int type,byte subclass,out uint rgba)
{
rgba=0;
try
{
var count=Read<ulong>(tree+8);var root=Pointer(tree+0x10);
if(count==0||count>65536||root<0x10000)return false;
var node=root;
for(var step=0;step<128&&node!=0;step++)
{
if(node<0x10000||node==tree+0x10)return false;
var key=Read<int>(node+0x1C);var minor=Read<byte>(node+0x20);
if(type==key&&subclass==minor)
{
var value=Read<uint>(node+0x2C);
if(Pointer(tree+0x10)!=root||Read<ulong>(tree+8)!=count||
Read<int>(node+0x1C)!=key||Read<byte>(node+0x20)!=minor||Read<uint>(node+0x2C)!=value)return false;
rgba=value;return true;
}
var next=Pointer(node+(type<key||type==key&&subclass<minor?8u:0u));
if(next==node)return false;node=next;
}
}
catch{}
return false;
}
static int GalaxyAppearanceLuminosityGroup(ushort classification)
{
var luminosity=(classification&0x7C00)>>10;
return luminosity switch
{
0 or 2=>1,1 or 3 or 5=>2,4=>3,
>=6 and <=9=>7,>=10 and <=13=>4,>=14 and <=17=>5,
>=18 and <=22=>0,23=>6,_=>-1
};
}
static bool GalaxyAppearanceTemperatureKey(nuint expectedImage,uint rawTemperature,out int type,out byte subclass)
{
type=0;subclass=0;
try
{
var first=Read<uint>(expectedImage+0x5FDF490);var second=Read<uint>(expectedImage+0x5FDF494);
if(first==0||second<=first)return false;
ulong key;
if(rawTemperature>=second)key=Read<ulong>(expectedImage+0x5FDF4C8);
else
{
var index=rawTemperature<first?0u:1u;
var descriptor=expectedImage+0x5FDF498+index*0x18;
var minimum=Read<uint>(descriptor);var values=Pointer(descriptor+8);
var step=Read<uint>(expectedImage+0x5539320+index*4);
if(step is not(10 or 100)||values<0x10000)return false;
var bucket=rawTemperature/step;
if(bucket<minimum||bucket-minimum>4095)return false;
// This is the exact address expression used by 3C3F5A0.
// Native reads are guarded by XfeCopy; no search/load occurs.
var at=values+(nuint)(bucket-minimum)*8;key=Read<ulong>(at);
if(Pointer(descriptor+8)!=values||Read<uint>(descriptor)!=minimum||Read<ulong>(at)!=key)return false;
}
type=(int)(uint)key;subclass=(byte)(key>>32);
return type is >=0 and <44&&subclass<=9&&Read<uint>(expectedImage+0x5FDF490)==first&&Read<uint>(expectedImage+0x5FDF494)==second;
}
catch{return false;}
}
static bool GalaxyAppearancePhysicalColour(nuint expectedImage,ushort classification,uint rawTemperature,uint typeRgba,out uint rgba)
{
rgba=typeRgba;var group=GalaxyAppearanceLuminosityGroup(classification);
if(group<0)return true;
var type=classification&63;var subclass=(byte)Math.Min((classification>>6)&15,9);
if(type is 9 or 10&&!GalaxyAppearanceTemperatureKey(expectedImage,rawTemperature,out type,out subclass))return false;
var offset=group switch{0=>0x5FDF460u,1=>0x5FDF2A0u,2=>0x5FDF100u,3=>0x5FDF4E0u,
4=>0x5FDF190u,5=>0x5FDF330u,6=>0x5FDF560u,7=>0x5FDF3E0u,_=>0u};
return GalaxyAppearanceTree(expectedImage+offset,type,subclass,out rgba);
}
static uint GalaxyAppearanceMix(uint typeRgba,uint physicalRgba,float blend)
{
var result=0u;var inverse=1f-blend;
for(var channel=0;channel<4;channel++)
{
var shift=channel*8;
var first=((typeRgba>>shift)&255)*(1f/255f);
var second=((physicalRgba>>shift)&255)*(1f/255f);
var value=(uint)(int)Math.Clamp((first*inverse+second*blend)*255f,0f,255f);
result|=value<<shift;
}
return result;
}
static float GalaxyAppearanceSize(uint rawScale,int type,bool small)
{
// Raw field +34 is kept under its original neutral name. Original
// 48B5BC0 is the scalar natural logarithm (near-one path x-x^2/2...).
var value=type<40||type==42?rawScale*(1f/32768f):type==40?rawScale*(1f/32768f)*1.4e-6f:1.1920928955078125e-7f;
var log=MathF.Log(value);var normal=(Math.Clamp(log,-3f,8f)+3f)/11f;
return normal*(small?1.5f-.05f:1.2f-.3f)+(small?.05f:.3f);
}
static bool TryCopyGalaxyAppearance(GalaxyServices services,nuint compact,out GalaxyAppearance result)
{
result=default;
if(!Enabled()||!inGame||owningThread==0||GetCurrentThreadId()!=owningThread||compact<0x10000||services.registry<0x10000)return false;
try
{
var id=Read<ulong>(compact);var classification=Read<ushort>(compact+0x40);var type=classification&63;
if(id==0||id>=0x80000000000000UL||type>=48)return false;
var temperature=Read<uint>(compact+0x30);var rawScale=Read<uint>(compact+0x34);
var typeColour=Read<uint>(image+0x5539330+(nuint)type*4);
var source=GalaxyAppearanceSettings(services);uint? realistic=null;
if(source.blend is{} blend&&GalaxyAppearancePhysicalColour(image,classification,temperature,typeColour,out var physical))
realistic=GalaxyAppearanceMix(typeColour,physical,blend);
if(Read<ulong>(compact)!=id||Read<ushort>(compact+0x40)!=classification||
Read<uint>(compact+0x30)!=temperature||Read<uint>(compact+0x34)!=rawScale)return false;
var membership=ReadGalaxyNavRouteMembership(services,id,out var routeViewMode);
result=new(typeColour,realistic,source.blend,rawScale,classification,
GalaxyAppearanceSize(rawScale,type,false),GalaxyAppearanceSize(rawScale,type,true),source.small)
{navRouteMembership=membership,navRouteViewMode=routeViewMode,navRouteMembershipMode=routeViewMode is{} view?view==1?1:0:null};
return true;
}
catch{return false;}
}
}
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
// Original 6891 image: 3B26700/3C415F0 (colour), 3C3F5A0/3C3F440
// (luminosity/subclass palette), 3B28090/3B3E640 (size). These methods
// copy only already loaded records. In particular, the original tree
// operator 3C284E0 INSERTS a missing key, so it is never invoked here.
internal readonly record struct GalaxyAppearance(uint typeRgba,uint? realisticRgba,float? colourBlend,
uint rawScale,ushort classification,float size,float smallSize,bool? smallSizeActive)
{
internal bool? navRouteMembership {get;init;}
internal int? navRouteMembershipMode {get;init;}
internal int? navRouteViewMode {get;init;}
}
readonly record struct GalaxyAppearanceSource(nuint registry,nuint expectedImage,ulong tick,float? blend,bool? small);
static GalaxyAppearanceSource galaxyAppearanceSource;
static GalaxyAppearanceSource GalaxyAppearanceSettings(GalaxyServices services)
{
var now=GetTickCount64();var cached=galaxyAppearanceSource;
if(cached.registry==services.registry&&cached.expectedImage==image&&now-cached.tick<100)return cached;
float? blend=null;bool? small=null;
try
{
// 3A06740: base+58 main interface; base+60 type interface.
// 3A55CF0 (main vtable+D8) returns float(main+99C).
var component=RegisteredComponent(services.registry,Read<uint>(image+0x5FA0A10));
if(component>=0x10060&&Pointer(component)==image+0x551F518&&Pointer(component-8)==image+0x551F3D8&&
Pointer(component-0x60)==image+0x551F2A8&&Pointer(component-0x58)==image+0x551F2D8)
{
var value=Read<float>(component-8+0x99C);
if(float.IsFinite(value)&&value>=0&&value<=1)blend=value;
}
// 3B3E640 calls the same settings interface+158. The live
// implementation 33D8280 reads byte(iface+624), while the
// alternate 552AE90 implementation returns constant true.
component=RegisteredComponent(services.registry,Read<uint>(image+0x5F05DD4));
if(component>=0x10000)
{
var table=Pointer(component);
if(table==image+0x552A9B8&&Pointer(table+0x158)==image+0x33D8220)
{
var value=Read<byte>(component+0x624);if(value<=1)small=value!=0;
}
else if(table==image+0x552AE60&&Pointer(table+0x158)==image+0x201130)small=true;
}
}
catch{}
var result=new GalaxyAppearanceSource(services.registry,image,now,blend,small);
galaxyAppearanceSource=result;return result;
}
static bool GalaxyAppearanceTree(nuint tree,int type,byte subclass,out uint rgba)
{
rgba=0;
try
{
var count=Read<ulong>(tree+8);var root=Pointer(tree+0x10);
if(count==0||count>65536||root<0x10000)return false;
var node=root;
for(var step=0;step<128&&node!=0;step++)
{
if(node<0x10000||node==tree+0x10)return false;
var key=Read<int>(node+0x1C);var minor=Read<byte>(node+0x20);
if(type==key&&subclass==minor)
{
var value=Read<uint>(node+0x2C);
if(Pointer(tree+0x10)!=root||Read<ulong>(tree+8)!=count||
Read<int>(node+0x1C)!=key||Read<byte>(node+0x20)!=minor||Read<uint>(node+0x2C)!=value)return false;
rgba=value;return true;
}
var next=Pointer(node+(type<key||type==key&&subclass<minor?8u:0u));
if(next==node)return false;node=next;
}
}
catch{}
return false;
}
static int GalaxyAppearanceLuminosityGroup(ushort classification)
{
var luminosity=(classification&0x7C00)>>10;
return luminosity switch
{
0 or 2=>1,1 or 3 or 5=>2,4=>3,
>=6 and <=9=>7,>=10 and <=13=>4,>=14 and <=17=>5,
>=18 and <=22=>0,23=>6,_=>-1
};
}
static bool GalaxyAppearanceTemperatureKey(nuint expectedImage,uint rawTemperature,out int type,out byte subclass)
{
type=0;subclass=0;
try
{
var first=Read<uint>(expectedImage+0x5FDF490);var second=Read<uint>(expectedImage+0x5FDF494);
if(first==0||second<=first)return false;
ulong key;
if(rawTemperature>=second)key=Read<ulong>(expectedImage+0x5FDF4C8);
else
{
var index=rawTemperature<first?0u:1u;
var descriptor=expectedImage+0x5FDF498+index*0x18;
var minimum=Read<uint>(descriptor);var values=Pointer(descriptor+8);
var step=Read<uint>(expectedImage+0x5539320+index*4);
if(step is not(10 or 100)||values<0x10000)return false;
var bucket=rawTemperature/step;
if(bucket<minimum||bucket-minimum>4095)return false;
// This is the exact address expression used by 3C3F5A0.
// Native reads are guarded by XfeCopy; no search/load occurs.
var at=values+(nuint)(bucket-minimum)*8;key=Read<ulong>(at);
if(Pointer(descriptor+8)!=values||Read<uint>(descriptor)!=minimum||Read<ulong>(at)!=key)return false;
}
type=(int)(uint)key;subclass=(byte)(key>>32);
return type is >=0 and <44&&subclass<=9&&Read<uint>(expectedImage+0x5FDF490)==first&&Read<uint>(expectedImage+0x5FDF494)==second;
}
catch{return false;}
}
static bool GalaxyAppearancePhysicalColour(nuint expectedImage,ushort classification,uint rawTemperature,uint typeRgba,out uint rgba)
{
rgba=typeRgba;var group=GalaxyAppearanceLuminosityGroup(classification);
if(group<0)return true;
var type=classification&63;var subclass=(byte)Math.Min((classification>>6)&15,9);
if(type is 9 or 10&&!GalaxyAppearanceTemperatureKey(expectedImage,rawTemperature,out type,out subclass))return false;
var offset=group switch{0=>0x5FDF460u,1=>0x5FDF2A0u,2=>0x5FDF100u,3=>0x5FDF4E0u,
4=>0x5FDF190u,5=>0x5FDF330u,6=>0x5FDF560u,7=>0x5FDF3E0u,_=>0u};
return GalaxyAppearanceTree(expectedImage+offset,type,subclass,out rgba);
}
static uint GalaxyAppearanceMix(uint typeRgba,uint physicalRgba,float blend)
{
var result=0u;var inverse=1f-blend;
for(var channel=0;channel<4;channel++)
{
var shift=channel*8;
var first=((typeRgba>>shift)&255)*(1f/255f);
var second=((physicalRgba>>shift)&255)*(1f/255f);
var value=(uint)(int)Math.Clamp((first*inverse+second*blend)*255f,0f,255f);
result|=value<<shift;
}
return result;
}
static float GalaxyAppearanceSize(uint rawScale,int type,bool small)
{
// Raw field +34 is kept under its original neutral name. Original
// 48B5BC0 is the scalar natural logarithm (near-one path x-x^2/2...).
var value=type<40||type==42?rawScale*(1f/32768f):type==40?rawScale*(1f/32768f)*1.4e-6f:1.1920928955078125e-7f;
var log=MathF.Log(value);var normal=(Math.Clamp(log,-3f,8f)+3f)/11f;
return normal*(small?1.5f-.05f:1.2f-.3f)+(small?.05f:.3f);
}
static bool TryCopyGalaxyAppearance(GalaxyServices services,nuint compact,out GalaxyAppearance result)
{
result=default;
if(!Enabled()||!inGame||owningThread==0||GetCurrentThreadId()!=owningThread||compact<0x10000||services.registry<0x10000)return false;
try
{
var id=Read<ulong>(compact);var classification=Read<ushort>(compact+0x40);var type=classification&63;
if(id==0||id>=0x80000000000000UL||type>=48)return false;
var temperature=Read<uint>(compact+0x30);var rawScale=Read<uint>(compact+0x34);
var typeColour=Read<uint>(image+0x5539330+(nuint)type*4);
var source=GalaxyAppearanceSettings(services);uint? realistic=null;
if(source.blend is{} blend&&GalaxyAppearancePhysicalColour(image,classification,temperature,typeColour,out var physical))
realistic=GalaxyAppearanceMix(typeColour,physical,blend);
if(Read<ulong>(compact)!=id||Read<ushort>(compact+0x40)!=classification||
Read<uint>(compact+0x30)!=temperature||Read<uint>(compact+0x34)!=rawScale)return false;
var membership=ReadGalaxyNavRouteMembership(services,id,out var routeViewMode);
result=new(typeColour,realistic,source.blend,rawScale,classification,
GalaxyAppearanceSize(rawScale,type,false),GalaxyAppearanceSize(rawScale,type,true),source.small)
{navRouteMembership=membership,navRouteViewMode=routeViewMode,navRouteMembershipMode=routeViewMode is{} view?view==1?1:0:null};
return true;
}
catch{return false;}
}
}