diff --git a/src/EveDataProvider/DataTypes.tsx b/src/EveDataProvider/DataTypes.tsx
index db693d8..d0266fc 100644
--- a/src/EveDataProvider/DataTypes.tsx
+++ b/src/EveDataProvider/DataTypes.tsx
@@ -19,6 +19,7 @@ export interface TypeID {
groupID: number,
categoryID: number,
published: boolean,
+ factionID?: number,
marketGroupID?: number,
capacity?: number,
mass?: number,
@@ -26,6 +27,17 @@ export interface TypeID {
volume?: number,
}
+export interface GroupID {
+ name: string,
+ categoryID: number,
+ published: boolean,
+}
+
+export interface MarketGroup {
+ name: string,
+ parentGroupID?: number,
+}
+
export interface DogmaAttribute {
name: string
published: boolean,
diff --git a/src/EveDataProvider/EveDataProvider.stories.tsx b/src/EveDataProvider/EveDataProvider.stories.tsx
index 431d461..8737b86 100644
--- a/src/EveDataProvider/EveDataProvider.stories.tsx
+++ b/src/EveDataProvider/EveDataProvider.stories.tsx
@@ -18,6 +18,8 @@ const TestEveData = () => {
return (
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}
+ GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}
+ MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}
diff --git a/src/EveDataProvider/EveDataProvider.tsx b/src/EveDataProvider/EveDataProvider.tsx
index bc39291..2d35cd5 100644
--- a/src/EveDataProvider/EveDataProvider.tsx
+++ b/src/EveDataProvider/EveDataProvider.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { DogmaAttribute, DogmaEffect, TypeDogma, TypeID } from "./DataTypes";
+import { DogmaAttribute, DogmaEffect, GroupID, MarketGroup, TypeDogma, TypeID } from "./DataTypes";
import { defaultDataUrl } from "../settings";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -11,6 +11,8 @@ import * as esf_pb2 from "./esf_pb2.js";
interface DogmaData {
loaded?: boolean;
typeIDs?: Record
;
+ groupIDs?: Record;
+ marketGroups?: Record;
typeDogma?: Record;
dogmaEffects?: Record;
dogmaAttributes?: Record;
@@ -37,6 +39,8 @@ async function fetchDataFile(dataUrl: string, name: string, pb2: any): Promise {
}
fetchAndLoadDataFile("typeIDs", esf_pb2.esf.TypeIDs);
+ fetchAndLoadDataFile("groupIDs", esf_pb2.esf.GroupIDs);
+ fetchAndLoadDataFile("marketGroups", esf_pb2.esf.MarketGroups);
fetchAndLoadDataFile("typeDogma", esf_pb2.esf.TypeDogma);
fetchAndLoadDataFile("dogmaEffects", esf_pb2.esf.DogmaEffects);
fetchAndLoadDataFile("dogmaAttributes", esf_pb2.esf.DogmaAttributes);
diff --git a/src/EveDataProvider/esf_pb2.js b/src/EveDataProvider/esf_pb2.js
index 9e8ae12..e73575e 100644
--- a/src/EveDataProvider/esf_pb2.js
+++ b/src/EveDataProvider/esf_pb2.js
@@ -263,6 +263,7 @@ export const esf = $root.esf = (() => {
TypeID.prototype.groupID = 0;
TypeID.prototype.categoryID = 0;
TypeID.prototype.published = false;
+ TypeID.prototype.factionID = 0;
TypeID.prototype.marketGroupID = 0;
TypeID.prototype.capacity = 0;
TypeID.prototype.mass = 0;
@@ -293,22 +294,26 @@ export const esf = $root.esf = (() => {
break;
}
case 5: {
- m.marketGroupID = r.int32();
+ m.factionID = r.int32();
break;
}
case 6: {
- m.capacity = r.float();
+ m.marketGroupID = r.int32();
break;
}
case 7: {
- m.mass = r.float();
+ m.capacity = r.float();
break;
}
case 8: {
- m.radius = r.float();
+ m.mass = r.float();
break;
}
case 9: {
+ m.radius = r.float();
+ break;
+ }
+ case 10: {
m.volume = r.float();
break;
}
@@ -334,6 +339,211 @@ export const esf = $root.esf = (() => {
return TypeIDs;
})();
+ esf.GroupIDs = (function() {
+
+ function GroupIDs(p) {
+ this.entries = {};
+ if (p)
+ for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
+ if (p[ks[i]] != null)
+ this[ks[i]] = p[ks[i]];
+ }
+
+ GroupIDs.prototype.entries = emptyObject;
+
+ GroupIDs.decode = async function decode(r, l) {
+ if (!(r instanceof $Reader))
+ r = $Reader.create(r);
+ var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs(), k, value;
+ while (r.pos < c) {
+ if (r.need_data()) {
+ await r.fetch_data();
+ }
+ if (r.is_eof()) break;
+
+ var t = r.uint32();
+ switch (t >>> 3) {
+ case 1: {
+ if (m.entries === emptyObject)
+ m.entries = {};
+ var c2 = r.uint32() + r.pos;
+ k = 0;
+ value = null;
+ while (r.pos < c2) {
+ var tag2 = r.uint32();
+ switch (tag2 >>> 3) {
+ case 1:
+ k = r.int32();
+ break;
+ case 2:
+ value = $root.esf.GroupIDs.GroupID.decode(r, r.uint32());
+ break;
+ default:
+ r.skipType(tag2 & 7);
+ break;
+ }
+ }
+ m.entries[k] = value;
+ break;
+ }
+ default:
+ r.skipType(t & 7);
+ break;
+ }
+ }
+ return m;
+ };
+
+ GroupIDs.GroupID = (function() {
+
+ function GroupID(p) {
+ if (p)
+ for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
+ if (p[ks[i]] != null)
+ this[ks[i]] = p[ks[i]];
+ }
+
+ GroupID.prototype.name = "";
+ GroupID.prototype.categoryID = 0;
+ GroupID.prototype.published = false;
+
+ GroupID.decode = function decode(r, l) {
+ if (!(r instanceof $Reader))
+ r = $Reader.create(r);
+ var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs.GroupID();
+ while (r.pos < c) {
+ var t = r.uint32();
+ switch (t >>> 3) {
+ case 1: {
+ m.name = r.string();
+ break;
+ }
+ case 2: {
+ m.categoryID = r.int32();
+ break;
+ }
+ case 3: {
+ m.published = r.bool();
+ break;
+ }
+ default:
+ r.skipType(t & 7);
+ break;
+ }
+ }
+ if (!m.hasOwnProperty("name"))
+ throw Error("missing required 'name'", { instance: m });
+ if (!m.hasOwnProperty("categoryID"))
+ throw Error("missing required 'categoryID'", { instance: m });
+ if (!m.hasOwnProperty("published"))
+ throw Error("missing required 'published'", { instance: m });
+ return m;
+ };
+
+ return GroupID;
+ })();
+
+ return GroupIDs;
+ })();
+
+ esf.MarketGroups = (function() {
+
+ function MarketGroups(p) {
+ this.entries = {};
+ if (p)
+ for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
+ if (p[ks[i]] != null)
+ this[ks[i]] = p[ks[i]];
+ }
+
+ MarketGroups.prototype.entries = emptyObject;
+
+ MarketGroups.decode = async function decode(r, l) {
+ if (!(r instanceof $Reader))
+ r = $Reader.create(r);
+ var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups(), k, value;
+ while (r.pos < c) {
+ if (r.need_data()) {
+ await r.fetch_data();
+ }
+ if (r.is_eof()) break;
+
+ var t = r.uint32();
+ switch (t >>> 3) {
+ case 1: {
+ if (m.entries === emptyObject)
+ m.entries = {};
+ var c2 = r.uint32() + r.pos;
+ k = 0;
+ value = null;
+ while (r.pos < c2) {
+ var tag2 = r.uint32();
+ switch (tag2 >>> 3) {
+ case 1:
+ k = r.int32();
+ break;
+ case 2:
+ value = $root.esf.MarketGroups.MarketGroup.decode(r, r.uint32());
+ break;
+ default:
+ r.skipType(tag2 & 7);
+ break;
+ }
+ }
+ m.entries[k] = value;
+ break;
+ }
+ default:
+ r.skipType(t & 7);
+ break;
+ }
+ }
+ return m;
+ };
+
+ MarketGroups.MarketGroup = (function() {
+
+ function MarketGroup(p) {
+ if (p)
+ for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
+ if (p[ks[i]] != null)
+ this[ks[i]] = p[ks[i]];
+ }
+
+ MarketGroup.prototype.name = "";
+ MarketGroup.prototype.parentGroupID = 0;
+
+ MarketGroup.decode = function decode(r, l) {
+ if (!(r instanceof $Reader))
+ r = $Reader.create(r);
+ var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups.MarketGroup();
+ while (r.pos < c) {
+ var t = r.uint32();
+ switch (t >>> 3) {
+ case 1: {
+ m.name = r.string();
+ break;
+ }
+ case 2: {
+ m.parentGroupID = r.int32();
+ break;
+ }
+ default:
+ r.skipType(t & 7);
+ break;
+ }
+ }
+ if (!m.hasOwnProperty("name"))
+ throw Error("missing required 'name'", { instance: m });
+ return m;
+ };
+
+ return MarketGroup;
+ })();
+
+ return MarketGroups;
+ })();
+
esf.DogmaAttributes = (function() {
function DogmaAttributes(p) {
diff --git a/src/HullListing/HullListing.module.css b/src/HullListing/HullListing.module.css
new file mode 100644
index 0000000..78bd1c2
--- /dev/null
+++ b/src/HullListing/HullListing.module.css
@@ -0,0 +1,86 @@
+.listing {
+ background-color: #111111;
+ color: #c5c5c5;
+ font-size: 15px;
+ height: 100%;
+ position: relative;
+ width: 100%;
+}
+
+.listingContent {
+ height: calc(100% - 42px);
+ padding-right: 20px;
+ overflow-y: auto;
+}
+
+.header1, .header2, .header3 {
+ display: flex;
+ padding-left: 10px;
+ user-select: none;
+}
+.header1, .header2 {
+ height: 25px;
+ line-height: 25px;
+}
+
+.header1 {
+ background-color: #1d1d1d;
+}
+
+.header1:hover, .header2:hover, .header3:hover {
+ background-color: #4f4f4f;
+}
+
+.collapsed {
+ display: none;
+}
+
+.level1 {
+ padding-left: 20px;
+}
+.level2 {
+ padding-left: 40px;
+}
+.level3 {
+ padding-left: 60px;
+}
+
+.hull {
+ display: flex;
+}
+.hull > span {
+ display: inline-block;
+ height: 32px;
+ line-height: 32px;
+ margin-right: 6px;
+}
+.hull > span:nth-child(2) {
+ flex: 1;
+}
+.hull > span:last-child {
+ text-align: right;
+}
+.hull > span:last-child > img {
+ opacity: 0.5;
+}
+.hull > span:last-child > img:hover {
+ opacity: 1.0;
+}
+
+.hullSimulate {
+ cursor: pointer;
+}
+
+.topbar {
+ display: flex;
+}
+
+.topbar > input {
+ background-color: #1d1d1d;
+ color: #c5c5c5;
+ flex: 1;
+ height: 24px;
+ line-height: 24px;
+ margin: 6px;
+ padding-left: 6px;
+}
diff --git a/src/HullListing/HullListing.stories.tsx b/src/HullListing/HullListing.stories.tsx
new file mode 100644
index 0000000..94a1806
--- /dev/null
+++ b/src/HullListing/HullListing.stories.tsx
@@ -0,0 +1,34 @@
+import type { Decorator, Meta, StoryObj } from '@storybook/react';
+import React from "react";
+
+import { EsiProvider } from '../EsiProvider';
+import { HullListing } from './';
+import { EveDataProvider } from '../EveDataProvider';
+
+const meta: Meta = {
+ component: HullListing,
+ tags: ['autodocs'],
+ title: 'Component/HullListing',
+};
+
+export default meta;
+type Story = StoryObj;
+
+const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void }> = (Story) => {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export const Default: Story = {
+ args: {
+ changeHull: (typeId: number) => console.log(`changeHull(${typeId})`),
+ },
+ decorators: [withEsiProvider],
+};
diff --git a/src/HullListing/HullListing.tsx b/src/HullListing/HullListing.tsx
new file mode 100644
index 0000000..e73f80e
--- /dev/null
+++ b/src/HullListing/HullListing.tsx
@@ -0,0 +1,144 @@
+import clsx from "clsx";
+import React from "react";
+
+import { EveDataContext } from "../EveDataProvider";
+import { Icon } from "../Icon";
+
+import styles from "./HullListing.module.css";
+
+interface ListingHulls {
+ [typeId: string]: string;
+}
+
+interface ListingGroup {
+ [raceName: string]: ListingHulls;
+}
+
+interface ListingGroups {
+ [groupName: string]: ListingGroup;
+}
+
+const factionIdToRace: Record = {
+ 500001: "Caldari",
+ 500002: "Minmatar",
+ 500003: "Amarr",
+ 500004: "Gallente",
+} as const;
+
+const Hull = (props: { typeId: number, name: string, changeHull: (typeId: number) => void }) => {
+ const [expanded, setExpanded] = React.useState(false);
+
+ return
+
setExpanded((current) => !current)}>
+
+
+
+
+ {props.name}
+
+
props.changeHull(props.typeId)}>
+
+
+
+
+
+
+}
+
+const HullRace = (props: { name: string, entries: ListingHulls, changeHull: (typeId: number) => void }) => {
+ const [expanded, setExpanded] = React.useState(false);
+
+ if (props.entries === undefined) return null;
+
+ let children = <>>;
+ if (expanded) {
+ children = <>{Object.keys(props.entries).sort((a, b) => props.entries[a].localeCompare(props.entries[b])).map((typeId) => {
+ const name = props.entries[typeId];
+ return
+ })}>;
+ }
+
+ return
+
setExpanded((current) => !current)}>
+ {props.name} [{Object.keys(props.entries).length}]
+
+
+ {children}
+
+
+}
+
+const HullGroup = (props: { name: string, entries: ListingGroup, changeHull: (typeId: number) => void }) => {
+ const [expanded, setExpanded] = React.useState(false);
+
+ let children = <>>;
+ if (expanded) {
+ children = <>
+
+
+
+
+
+ >
+ }
+
+ return
+
setExpanded((current) => !current)}>
+ {props.name}
+
+
+ {children}
+
+
+};
+
+/**
+ * Show all the fittings for the current ESI character.
+ */
+export const HullListing = (props: { changeHull: (typeId: number) => void }) => {
+ const eveData = React.useContext(EveDataContext);
+
+ const [hullGroups, setHullGroups] = React.useState({});
+ const [search, setSearch] = React.useState("");
+
+ React.useEffect(() => {
+ if (!eveData.loaded) return;
+
+ const newHullGroups: ListingGroups = {};
+
+ for (const typeId in eveData.typeIDs) {
+ const hull = eveData.typeIDs[typeId];
+ if (hull.categoryID !== 6) continue;
+ if (hull.marketGroupID === undefined) continue;
+ if (!hull.published) continue;
+
+ if (search !== "" && !hull.name.toLowerCase().includes(search.toLowerCase())) continue;
+
+ const group = eveData.groupIDs?.[hull.groupID]?.name ?? "Unknown Group";
+ const race = factionIdToRace[hull.factionID || 0] ?? "NonEmpire";
+
+ if (newHullGroups[group] === undefined) {
+ newHullGroups[group] = {};
+ }
+ if (newHullGroups[group][race] === undefined) {
+ newHullGroups[group][race] = {};
+ }
+
+ newHullGroups[group][race][typeId] = hull.name;
+ }
+
+ setHullGroups(newHullGroups);
+ }, [eveData, search]);
+
+ return
+
+ setSearch(e.target.value)} />
+
+
+ {Object.keys(hullGroups).sort().map((groupName) => {
+ const groupData = hullGroups[groupName];
+ return
+ })}
+
+
+};
diff --git a/src/HullListing/index.ts b/src/HullListing/index.ts
new file mode 100644
index 0000000..1897994
--- /dev/null
+++ b/src/HullListing/index.ts
@@ -0,0 +1 @@
+export { HullListing } from "./HullListing";
diff --git a/src/Icon/Icon.tsx b/src/Icon/Icon.tsx
index 58f4641..41dddaf 100644
--- a/src/Icon/Icon.tsx
+++ b/src/Icon/Icon.tsx
@@ -20,6 +20,7 @@ const iconMapping = {
"shield-boost-rate": "texture/classes/fitting/statsicons/shieldboostrate.png",
"shield-hp": "texture/classes/fitting/statsicons/shieldhp.png",
"signature-radius": "texture/classes/fitting/statsicons/signatureradius.png",
+ "simulate": "texture/classes/fitting/iconsimulatorhover.png",
"thermal-resistance": "texture/classes/fitting/statsicons/thermalresistance.png",
"warp-speed": "texture/classes/fitting/statsicons/warpspeed.png",
} as const;
diff --git a/src/index.ts b/src/index.ts
index 86007b4..85bbf75 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,7 @@ export * from './EveDataProvider';
export * from './EveShipFitHash';
export * from './EveShipFitLink';
export * from './FormatEftToEsi';
+export * from './HullListing';
export * from './Icon';
export * from './ShipAttribute';
export * from './ShipFit';
diff --git a/src/settings.ts b/src/settings.ts
index 2d7a130..7770408 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -1 +1 @@
-export const defaultDataUrl = "https://data.eveship.fit/v1.2-20231115/";
+export const defaultDataUrl = "https://data.eveship.fit/v4.0-20231115/";