madden-franchise 3.6.0 → 3.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/FranchiseEnum.js CHANGED
@@ -90,6 +90,10 @@ class FranchiseEnum {
90
90
  };
91
91
 
92
92
  setMemberLength() {
93
+ if (this._members.length === 0) {
94
+ return;
95
+ }
96
+
93
97
  const maxValue = this._members.reduce((accum, currentVal) => {
94
98
  return (accum.value > currentVal.value ? accum : currentVal);
95
99
  });
package/FranchiseFile.js CHANGED
@@ -63,7 +63,7 @@ class FranchiseFile extends EventEmitter {
63
63
  this._rawContents = fs.readFileSync(filePath);
64
64
 
65
65
  /** @private @type {FileType} */
66
- this._type = getFileType(this._rawContents);
66
+ this._type = getFileType(this._rawContents, this._settings);
67
67
 
68
68
  /** @private @type {number} */
69
69
  this._gameYear = this._type.year;
@@ -81,7 +81,7 @@ class FranchiseFile extends EventEmitter {
81
81
  this.unpackedFileContents = unpackFile(this._rawContents, this._type);
82
82
 
83
83
  if (this._type.format === Constants.FORMAT.FRANCHISE_COMMON) {
84
- const newType = getFileType(this.unpackedFileContents);
84
+ const newType = getFileType(this.unpackedFileContents, this._settings);
85
85
  this._type.year = newType.year;
86
86
  this._gameYear = this._type.year;
87
87
  this._expectedSchemaVersion = getSchemaMetadata(
@@ -121,7 +121,11 @@ class FranchiseFile extends EventEmitter {
121
121
  ).path;
122
122
 
123
123
  try {
124
- this.schemaList = new FranchiseSchema(schemaPath);
124
+ this.schemaList = new FranchiseSchema(schemaPath, {
125
+ extraSchemas: this.settings.extraSchemas,
126
+ fileMap: this.settings.schemaFileMap,
127
+ useNewSchemaGeneration: this.settings.useNewSchemaGeneration
128
+ });
125
129
  this.schemaList.on("schemas:done", () => {
126
130
  resolve();
127
131
  });
@@ -583,12 +587,13 @@ function _saveSync(destination, packedContents) {
583
587
  /**
584
588
  *
585
589
  * @param {Buffer} data
590
+ * @param {FranchiseFileSettings} settings
586
591
  * @returns {FileType}
587
592
  */
588
- function getFileType(data) {
593
+ function getFileType(data, settings) {
589
594
  const isDataCompressed = isCompressed(data);
590
595
  const format = getFormat(data, isDataCompressed);
591
- const year = getGameYear(data, isDataCompressed, format);
596
+ const year = settings?.gameYearOverride ?? getGameYear(data, isDataCompressed, format);
592
597
 
593
598
  return {
594
599
  format: format,
@@ -652,7 +657,7 @@ function getGameYear(data, isCompressed, format) {
652
657
  max: 95,
653
658
  },
654
659
  {
655
- year: 25,
660
+ year: 26,
656
661
  max: 999,
657
662
  },
658
663
  ];
@@ -687,6 +692,8 @@ function getGameYear(data, isCompressed, format) {
687
692
  } else if (data[0x2A] === 0x35) {
688
693
  // M25 has year indicator in a different location
689
694
  return 25;
695
+ } else if (data[0x2A] === 0x36) {
696
+ return 26;
690
697
  } else {
691
698
  const schemaMajor = getCompressedSchema(data).major;
692
699
  const year = schemaMax.find((schema) => {
@@ -149,7 +149,7 @@ class FranchiseFileField {
149
149
  // return (formatted == 1 || (formatted.toString().toLowerCase() == 'true')) ? '1' : '0';
150
150
  actualValue = (value == 1 || (value.toString().toLowerCase() == 'true'));
151
151
  this._value = actualValue;
152
- this._unformattedValue.setBits(this.offset.offset, actualValue, 1);
152
+ this._unformattedValue.setBits(this.offset.offset + (this.offset.length - 1), actualValue, 1);
153
153
  break;
154
154
  case 'float':
155
155
  actualValue = parseFloat(value);
@@ -299,7 +299,7 @@ class FranchiseFileField {
299
299
  }
300
300
  }
301
301
  case 'bool':
302
- return unformatted.getBits(offset.offset, 1) ? true : false;
302
+ return unformatted.getBits(offset.offset + (offset.length - 1), 1) ? true : false;
303
303
  case 'float':
304
304
  // return utilService.bin2Float(unformatted);
305
305
  return unformatted.getFloat32(offset.offset, offset.length);
@@ -17,9 +17,17 @@ class FranchiseFileSettings {
17
17
  /** @type {string | false} */
18
18
  this.schemaDirectory = settings && settings.schemaDirectory ? settings.schemaDirectory : false;
19
19
  /** @type {boolean} */
20
+ this.useNewSchemaGeneration = settings?.useNewSchemaGeneration ?? false;
21
+ /** @type {Object} */
22
+ this.schemaFileMap = settings?.schemaFileMap || {};
23
+ /** @type {Object[]} */
24
+ this.extraSchemas = settings?.extraSchemas || undefined;
25
+ /** @type {boolean} */
20
26
  this.autoParse = settings && (settings.autoParse !== null && settings.autoParse !== undefined) ? settings.autoParse : true;
21
27
  /** @type {boolean} */
22
28
  this.autoUnempty = settings && (settings.autoUnempty !== null && settings.autoUnempty !== undefined) ? settings.autoUnempty : false;
29
+ /** @type {number} */
30
+ this.gameYearOverride = settings && (settings.gameYearOverride !== null && settings.gameYearOverride !== undefined) ? settings.gameYearOverride : null;
23
31
  }
24
32
  };
25
33
 
@@ -4,6 +4,7 @@ const zlib = require('zlib');
4
4
  const FranchiseEnum = require('./FranchiseEnum');
5
5
  const EventEmitter = require('events').EventEmitter;
6
6
  const schemaGenerator = require('./services/schemaGenerator');
7
+ const { generateSchemaV2 } = require('./services/schemaGeneratorV2');
7
8
 
8
9
  /**
9
10
  * @typedef SchemaAttribute
@@ -30,12 +31,15 @@ const schemaGenerator = require('./services/schemaGenerator');
30
31
  */
31
32
 
32
33
  class FranchiseSchema extends EventEmitter {
33
- constructor (filePath) {
34
+ constructor (filePath, { useNewSchemaGeneration = false, extraSchemas = [], fileMap = {} } = {}) {
34
35
  super();
35
36
  this.schemas = [];
36
37
  this.path = filePath;
38
+ this.useNewSchemaGeneration = useNewSchemaGeneration;
39
+ this.extraSchemas = extraSchemas;
40
+ this.fileMap = fileMap;
37
41
  };
38
-
42
+
39
43
  evaluate () {
40
44
  const fileExtension = path.extname(this.path).toLowerCase();
41
45
 
@@ -109,15 +113,28 @@ class FranchiseSchema extends EventEmitter {
109
113
  };
110
114
 
111
115
  evaluateSchemaXml() {
112
- schemaGenerator.eventEmitter.on('schemas:done', (schema) => {
113
- this.schema = schema;
114
- this.meta = schema.meta;
115
- this.schemas = schema.schemas;
116
- this.schemaMap = schema.schemaMap;
117
- this.emit('schemas:done');
118
- });
119
-
120
- schemaGenerator.generate(this.path);
116
+ if (this.useNewSchemaGeneration) {
117
+ generateSchemaV2({
118
+ fileMap: this.fileMap,
119
+ extraSchemas: this.extraSchemas
120
+ }).then((schema) => {
121
+ this.schema = schema;
122
+ this.meta = schema.meta;
123
+ this.schemas = schema.schemas;
124
+ this.schemaMap = schema.schemaMap;
125
+ this.emit('schemas:done');
126
+ });
127
+ } else {
128
+ schemaGenerator.eventEmitter.on('schemas:done', (schema) => {
129
+ this.schema = schema;
130
+ this.meta = schema.meta;
131
+ this.schemas = schema.schemas;
132
+ this.schemaMap = schema.schemaMap;
133
+ this.emit('schemas:done');
134
+ });
135
+
136
+ schemaGenerator.generate(this.path);
137
+ }
121
138
  };
122
139
  };
123
140
 
package/README.md CHANGED
@@ -35,6 +35,7 @@ madden-franchise is a Madden franchise file parser written in NodeJS that allows
35
35
  | Madden 23 | ✅ Full |
36
36
  | Madden 24 | ✅ Full |
37
37
  | Madden 25 | ✅ Full |
38
+ | Madden 26 | 🟡 Everything but CharacterVisuals |
38
39
 
39
40
  ### Quick Start
40
41
  #### Initializing
@@ -65,6 +66,22 @@ madden-franchise is a Madden franchise file parser written in NodeJS that allows
65
66
  // AUTO UNEMPTY - specify if you want the system to automatically determine if an empty field should become un-empty once you edit it.
66
67
  // Warning: may have unintended side-effects if you batch import. Enable with caution.
67
68
  autoUnempty: true/false [default: false]
69
+
70
+ // USE NEW SCHEMA GENERATION - use v2 schema generation, may cause issues
71
+ useNewSchemaGeneration: true/false [default: false]
72
+
73
+ // SCHEMA FIELD MAP - used with v2 schema generation - specify all schema input files. Object key should be either `main` to specify the main schema, or the name of the file as specified in the `<IncludeFile>`. Values should be absolute paths to the schema files.
74
+ schemaFileMap: {
75
+ main: '/abs/path/to/franchise-schemas.ftx',
76
+ 'core-schemas': '/abs/path/to/core-schemas.ftx',
77
+ ...
78
+ }
79
+
80
+ // EXTRA SCHEMAS - used with v2 schema generation to specify extra schemas to include other than the defaults. Warning: if these are included, the default extra schemas will NOT be included and vice-versa.
81
+ extraSchemas: [...]
82
+
83
+ // GAME YEAR OVERRIDE - use to override the game year to adjust schema picker logic. Helpful for FTC files and other files that can't auto-detect year accurately.
84
+ gameYearOverride: number
68
85
  }
69
86
 
70
87
  #### Terminology
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madden-franchise",
3
- "version": "3.6.0",
3
+ "version": "3.7.2",
4
4
  "description": "Tools to read a madden franchise file and get data from it",
5
5
  "main": "FranchiseFile.js",
6
6
  "scripts": {
@@ -14,6 +14,7 @@
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
16
  "bit-buffer": "^0.2.5",
17
+ "fast-xml-parser": "^5.2.5",
17
18
  "node-xml-stream-parser": "^1.0.12"
18
19
  },
19
20
  "repository": {
@@ -22,7 +23,6 @@
22
23
  },
23
24
  "devDependencies": {
24
25
  "chai": "^4.2.0",
25
- "chai-eventemitter": "^1.1.1",
26
26
  "mocha": "^9.1.4",
27
27
  "proxyquire": "^2.1.3",
28
28
  "sinon": "^7.5.0",
@@ -0,0 +1,44 @@
1
+ // USAGE:
2
+ // node schema-generator-script.js [input file folder] [output file folder] [main schema name] [extra schema path]
3
+ const fs = require('fs/promises');
4
+ const path = require('path');
5
+ const { gzipSync } = require('zlib');
6
+ const { generateSchemaV2 } = require('../services/schemaGeneratorV2');
7
+
8
+ let done = false;
9
+
10
+ (async () => {
11
+ const files = await fs.readdir(process.argv[2], { withFileTypes: true });
12
+ const schemas = files.filter(f => f.isFile() && f.name.endsWith('.FTX') && f.name !== 'football.FTX');
13
+ const mainSchemaName = process.argv[4] || 'franchise-schemas.FTX';
14
+
15
+ const main = schemas.find(f => f.name === mainSchemaName);
16
+ const otherSchemas = schemas
17
+ .filter(f => f.name !== mainSchemaName)
18
+ .reduce((acc, file) => {
19
+ const name = file.name.replace(/\.FTX$/i, '');
20
+ acc[name] = path.join(process.argv[2], file.name);
21
+ return acc;
22
+ }, {});
23
+
24
+ const schema = await generateSchemaV2({
25
+ fileMap: {
26
+ main: path.join(process.argv[2], main.name),
27
+ ...otherSchemas
28
+ },
29
+ extraSchemas: process.argv[5] ? JSON.parse(await fs.readFile(process.argv[5], 'utf8')) : undefined
30
+ });
31
+
32
+ const outputPath = process.argv[3] || __dirname;
33
+ const compressed = gzipSync(JSON.stringify(schema));
34
+ await fs.writeFile(path.join(outputPath, `M${schema.meta.gameYear}_${schema.meta.major}_${schema.meta.minor}.gz`), compressed);
35
+ done = true;
36
+ })();
37
+
38
+ function wait () {
39
+ if (!done) {
40
+ setTimeout(wait, 500);
41
+ }
42
+ };
43
+
44
+ wait();
@@ -0,0 +1,193 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const { XMLParser } = require('fast-xml-parser');
4
+ const FranchiseEnum = require('../FranchiseEnum');
5
+ const utilService = require('./utilService');
6
+
7
+ /**
8
+ * Parse FTX schema files with support for IncludeFile dependencies.
9
+ * @param {{main: string, [id:string]: string}} fileMap - { main: '/abs/path/to/main.FTX', ...otherName: '/abs/path/to/other.FTX' }
10
+ * @param {Object[]} [extraSchemas] - Optional array of extra schemas to include. If not included, will use defaults.
11
+ * @returns {Promise<Object>} - Parsed schema object
12
+ */
13
+ async function generateSchemaV2({ fileMap, extraSchemas }) {
14
+ const parsedFiles = {};
15
+ const enums = [];
16
+ const schemas = [];
17
+ const schemaMap = {};
18
+ let schemaMeta = {};
19
+
20
+ if (!extraSchemas || extraSchemas.length === 0) {
21
+ // Load extra schemas if available
22
+ try {
23
+ const extraPath = path.join(__dirname, '../data/schemas/extra-schemas.json');
24
+ const extraRaw = await fs.readFile(extraPath, 'utf8');
25
+ extraSchemas = JSON.parse(extraRaw);
26
+ } catch (e) {
27
+ // ignore if not present
28
+ }
29
+ }
30
+
31
+ // Recursively parse a file and its includes (async)
32
+ async function parseFile(fileKey) {
33
+ if (parsedFiles[fileKey]) return;
34
+ const filePath = fileMap[fileKey];
35
+ if (!filePath) throw new Error(`Missing file mapping for: ${fileKey}`);
36
+ const xml = await fs.readFile(filePath, 'utf8');
37
+ const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', allowBooleanAttributes: true, trimValues: false });
38
+ const doc = parser.parse(xml);
39
+ const root = doc.FranTkData;
40
+ if (!schemaMeta.databaseName && root.databaseName) {
41
+ schemaMeta = {
42
+ databaseName: root.databaseName,
43
+ dataMajorVersion: root.dataMajorVersion,
44
+ dataMinorVersion: root.dataMinorVersion
45
+ };
46
+ }
47
+ // Parse includes first
48
+ if (root.Includes && root.Includes.IncludeFile) {
49
+ const includes = Array.isArray(root.Includes.IncludeFile)
50
+ ? root.Includes.IncludeFile
51
+ : [root.Includes.IncludeFile];
52
+ for (const inc of includes) {
53
+ let incKey = inc.fileName.replace(/\.FTX$/i, '');
54
+ if (!fileMap[incKey]) {
55
+ // Try all lowercase
56
+ incKey = incKey.toLowerCase();
57
+ if (!fileMap[incKey]) {
58
+ console.warn(`schemaGeneratorV2: Missing file mapping for include: ${incKey} in ${fileKey}`);
59
+ continue; // Suppress missing include mapping
60
+ }
61
+ }
62
+ await parseFile(incKey);
63
+ }
64
+ }
65
+ // Parse enums and schemas
66
+ if (root.schemas) {
67
+ const items = Object.entries(root.schemas)
68
+ .flatMap(([tag, val]) => Array.isArray(val) ? val.map(v => ({ tag, ...v })) : [{ tag, ...val }]);
69
+ for (const item of items) {
70
+ if (item.tag === 'enum') {
71
+ const theEnum = new FranchiseEnum(item.name, item.assetId, item.isRecordPersistent);
72
+ theEnum._members = [];
73
+ if (item.attribute) {
74
+ const members = Array.isArray(item.attribute) ? item.attribute : [item.attribute];
75
+ for (const attr of members) {
76
+ theEnum.addMember && theEnum.addMember(attr.name, attr.idx, attr.value);
77
+ }
78
+ }
79
+ enums.push(theEnum);
80
+ } else if (item.tag === 'schema') {
81
+ const schema = {
82
+ assetId: item.assetId,
83
+ ownerAssetId: item.ownerAssetId,
84
+ numMembers: item.numMembers,
85
+ name: item.name,
86
+ base: item.base,
87
+ attributes: []
88
+ };
89
+ if (item.attribute) {
90
+ const attrs = Array.isArray(item.attribute) ? item.attribute : [item.attribute];
91
+ for (const attr of attrs) {
92
+ schema.attributes.push(parseAttribute(attr));
93
+ }
94
+ }
95
+ schemas.push(schema);
96
+ schemaMap[schema.name] = schema;
97
+ }
98
+ }
99
+ }
100
+ parsedFiles[fileKey] = true;
101
+ }
102
+
103
+ // Attribute parsing logic (copied from schemaGenerator.js)
104
+ function parseAttribute(attributeAttributes) {
105
+ return {
106
+ index: attributeAttributes.idx,
107
+ name: attributeAttributes.name,
108
+ type: attributeAttributes.type,
109
+ minValue: attributeAttributes.minValue,
110
+ maxValue: attributeAttributes.maxValue,
111
+ maxLength: attributeAttributes.maxLen,
112
+ default: getDefaultValue(attributeAttributes.default),
113
+ final: attributeAttributes.final,
114
+ enum: getEnum(attributeAttributes.type),
115
+ const: attributeAttributes.const
116
+ };
117
+ function getDefaultValue(defaultVal) {
118
+ if (!defaultVal) { return undefined; }
119
+ // Only replace XML entities, do not trim or modify whitespace at all
120
+ return defaultVal
121
+ .replace(/&#xD;/g, '\r')
122
+ .replace(/&#xA;/g, '\n')
123
+ .replace(/&amp;/g, '&')
124
+ .replace(/&gt;/g, '>')
125
+ .replace(/&lt;/g, '<')
126
+ .replace(/&quot;/g, '"');
127
+ }
128
+ }
129
+
130
+ function getEnum(name) {
131
+ return enums.find(theEnum => theEnum.name === name);
132
+ }
133
+
134
+ function addExtraSchemas() {
135
+ if (!Array.isArray(extraSchemas)) return;
136
+ extraSchemas.forEach((schema) => {
137
+ if (!schemaMap[schema.name]) {
138
+ schema.attributes.filter((attrib) => {
139
+ return attrib.enum && !(attrib.enum instanceof FranchiseEnum);
140
+ }).forEach((attrib) => {
141
+ attrib.enum = getEnum(attrib.enum);
142
+ });
143
+ schemas.unshift(schema);
144
+ }
145
+ });
146
+ }
147
+
148
+ function calculateInheritedSchemas(schemaList) {
149
+ const schemasWithBase = schemaList.filter((schema) => schema.base && schema.base.indexOf('()') === -1);
150
+ schemasWithBase.forEach((schema) => {
151
+ if (schema.base && schema.base.indexOf('()') === -1) {
152
+ schema.originalAttributesOrder = schema.attributes;
153
+ const baseSchema = schemaList.find((schemaToSearch) => schemaToSearch.name === schema.base);
154
+
155
+ if (baseSchema) {
156
+ baseSchema.attributes.forEach((baseAttribute, index) => {
157
+ let oldIndex = schema.attributes.findIndex((schemaAttribute) => schemaAttribute?.name === baseAttribute?.name);
158
+ utilService.arrayMove(schema.attributes, oldIndex, index);
159
+ });
160
+ }
161
+ }
162
+ });
163
+ }
164
+
165
+ await parseFile('main');
166
+ addExtraSchemas();
167
+ calculateInheritedSchemas(schemas);
168
+
169
+ // Set enum member lengths if needed
170
+ enums.forEach(e => e.setMemberLength && e.setMemberLength());
171
+
172
+ // Extract gameYear from databaseName
173
+ const majorVersion = schemaMeta.dataMajorVersion;
174
+ const minorVersion = schemaMeta.dataMinorVersion;
175
+ const databaseName = schemaMeta.databaseName;
176
+ const gameYearMatch = /Madden(\d{2})/.exec(databaseName);
177
+ const gameYear = gameYearMatch ? parseInt(gameYearMatch[1]) : null;
178
+
179
+ const root = {
180
+ meta: {
181
+ major: parseInt(majorVersion),
182
+ minor: parseInt(minorVersion),
183
+ gameYear
184
+ },
185
+ schemas,
186
+ schemaMap
187
+ };
188
+
189
+ // Return the plain object
190
+ return root;
191
+ }
192
+
193
+ module.exports = { generateSchemaV2 };
@@ -25,6 +25,7 @@ StrategyPicker.pick = (type) => {
25
25
  case 24:
26
26
  return M24Strategy;
27
27
  case 25:
28
+ case 26:
28
29
  return M25Strategy;
29
30
  }
30
31
  }