Jump to content

Multiclass kits and the kit limit


DavidW

Recommended Posts

Partly for the sake of trying something new, I've been trying to get my head around how the kit system works on EE, especially with respect to multiclassed kits. Here's a quick summary of what I think is correct (and one proposal): if any of the various people who know this part of the engine better than me wanted to confirm (or deny!) that I'm understanding it correctly, I'd appreciate it.

1) The engine is perfectly happy for you to define a kit with a multiclass parent class.

2) The engine, and the chargen UI, are perfectly happy for you to assign a multiclass or single-class kit to the kit-selection 2da for a multiclass. 

3) If you attach a single-class kit to a multiclass character, it overrides the relevant component class. So a Fighter/Mage could become a Berserker/Mage or a Fighter/Invoker, and the character will be listed as Fighter level X... Invoker level Y on the character page. (Indeed, that's how the existing Fighter/Illusionist is defined.) Since the component classes of a multiclass character determine the CLAB table, you end up using the kit's CLAB table for the relevant class. But since the multiclass controls the LUA table, you still use the vanilla multiclass HLAs.

4) Conversely, if you attach a multiclass kit to a multiclass character, it overrides the multiclass. So a Fighter/Mage could become... well, anything you like... though the character will still be listed as Fighter level X... Mage level Y on the character page. Since the component classes are unchanged, you draw the CLAB entries from the vanilla tables for the component classes, and any kit-defined CLAB is ignored (though of course you can do kit-specific abilities using a 318/326-gated AP_ in the component-class CLAB - I assume that's how QDMULTI works, though I haven't looked at the code). Since the multiclass is changed, you can sub in a kit-specific LUA. 

5) The engine sulks (i.e. crashes) if you try to use a CLAB file defined in a kitlist.2da entry higher than 255. But it's otherwise happy to use kitlist entries as high as you like: it will cheerfully read names, descriptions, IDS entries etc from high-numbered kitlist rows. That means that the ceiling of 255 kits applies only to single-class kits: you can have as many multiclass kits as you like. BUT the single-class kits need to be first in kitlist.2da, or at least, any spaces after row 255 have to be for multiclass kits. 

(I appreciate there's also a problem if you use WEIDU's built-in ADD_KIT command, which is hardcoded to complain if you go above 255 kits. SFO's make_kit function doesn't have that problem, and I think argent77 also has a kit-adding function that avoids the problem?)

If all that's right, presumably it would be sensible to sort kitlist.ids (and make the necessary adjustments to the k_x_y.2da files) after installing a single-class kit, so as to make sure single-class kits precede multiclass kits? (Otherwise you get quite a strong install-order constraint - install single-class kits first - albeit it only matters on quite heavily modded installs.) Am I missing any problem or subtlety that would arise from doing so? Has someone already written code to do it? (If not, I might write some myself.)

Link to comment

1) Correct

2) I don't understand what you mean.  You mean the "K_F_H.2da" etc. files?  In which case, the EE 2.5+ engine is happy to display multiclass kits when generating multiclass characters.  I've never tried putting a single-class kit into e.g. K_FM_HE.2da, don't know if that works.  The pre-EE engine will not make multiclass kits available in Chargen, AFAIK.

3) Correct.

4) Correct.  QDMulti adds a single AP_ spell to the core kitless class tables, and then populates each single spell at each level with opcode 177 effects targeting multiclass kits that have kit effects at that level.  The 177 effects reference .EFFs that use opcode 146 or opcode 171, representing the AP_ and GA_ entries of a multiclass kit table, respectively.

5) Correct.

2 hours ago, DavidW said:

(I appreciate there's also a problem if you use WEIDU's built-in ADD_KIT command, which is hardcoded to complain if you go above 255 kits. SFO's make_kit function doesn't have that problem, and I think argent77 also has a kit-adding function that avoids the problem?)

Yes, ADD_KIT_EX will happily do it, and Weidu's built-in ADD_KIT will happily do it as well if you fool it into thinking ToBEx is installed by adding /tobex_ini/tobexver.txt.  (Can delete it afterward to keep the game folder clean.)

2 hours ago, DavidW said:

presumably it would be sensible to sort kitlist.ids (and make the necessary adjustments to the k_x_y.2da files) after installing a single-class kit, so as to make sure single-class kits precede multiclass kits? (Otherwise you get quite a strong install-order constraint - install single-class kits first - albeit it only matters on quite heavily modded installs.) Am I missing any problem or subtlety that would arise from doing so? Has someone already written code to do it? (If not, I might write some myself.)

There was discussion about doing this - the idea raised was, fill out kitlist.2da with dummy entries when a multiclass kit is installed, so that all multiclass kits are installed at entry #256+; and then jazz up ADD_KIT to automatically replace the dummy entries when installing single-class kits.  The concerns that kept it in the realm of the hypothetical were 1) the effort involved, and 2) compatibility with legacy mods.  Ultimately the problem of having too many kits is pretty rare, and fairly easily addressed by paying attention to install order.  So there hasn't been much motivation to put more effort into the issue.

Link to comment
5 minutes ago, subtledoctor said:

I've never tried putting a single-class kit into e.g. K_FM_HE.2da, don't know if that works. 

It does, so far as I can see. It's a very quick lightweight way of making a singleclass kit multiclassed, but it leaves out lots of features - the LUA table for one. I think it's mostly a curiosity.

 

6 minutes ago, subtledoctor said:

Correct.  QDMulti adds a single AP_ spell to the core kitless class tables, and then populates each single spell at each level with opcode 177 effects targeting multiclass kits that have kit effects at that level.  The 177 effects reference .EFFs that use opcode 146 or opcode 171, representing the AP_ and GA_ entries of a multiclass kit table, respectively.

Do you know if there's any particular advantage of using 177 and EFFs rather than just 326?

 

7 minutes ago, subtledoctor said:

There was discussion about doing this - the idea raised was, fill out kitlist.2da with dummy entries when a multiclass kit is installed, so that all multiclass kits are installed at entry #256+; and then jazz up ADD_KIT to automatically replace the dummy entries when installing single-class kits.  The concerns that kept it in the realm of the hypothetical were 1) the effort involved, and 2) compatibility with legacy mods.

That does sound messy. I was thinking of something simpler - just run a function that reorders kitlist.2da and relabels the numbers in the k_x_y.2da files. Then you could call it after installing a single-class kit, and (unless I'm missing something) there wouldn't be any legacy-mod problems. I don't think that would be too hard to do - I might see if I can work something up.

Link to comment

This works, I think.

DEFINE_ACTION_FUNCTION dw_reorder_kitlist BEGIN

	// parse kitlist to get number=>multiclas map
	ACTION_CLEAR_ARRAY is_multiclass
	COPY_EXISTING "kitlist.2da" override
		COUNT_2DA_COLS colcount
		READ_2DA_ENTRIES_NOW dw_kitlist_data colcount
		FOR (row=0;row<dw_kitlist_data;++row) BEGIN
			READ_2DA_ENTRY_FORMER dw_kitlist_data row 0 number
			READ_2DA_ENTRY_FORMER dw_kitlist_data row 8 class_id
			LPF class_is_multiclass STR_VAR class_id RET multiclass END
			PATCH_IF multiclass BEGIN
				SET $is_multiclass("%number%")=1
			END
		END
	BUT_ONLY
	
	// destructively parse kitlist to get the data
	ACTION_CLEAR_ARRAY kitlist_contents
	COPY_EXISTING "kitlist.2da" override
		REPLACE_EVALUATE "^\([0-9]+\)[ %TAB%]+\([^ %TAB%].*\)" BEGIN
			SPRINT $kitlist_contents("%MATCH1%") "%MATCH2%"
		END
		""	
	BUT_ONLY // obviously it will change, this is just for neatness

	ACTION_CLEAR_ARRAY kit_number_map
	OUTER_SET number_new=0
	OUTER_SPRINT kitlist_write ""

	// put the single-class data back

	ACTION_PHP_EACH kitlist_contents AS number=>data BEGIN
		ACTION_IF !VARIABLE_IS_SET $is_multiclass("%number%") BEGIN // single-class kit
			OUTER_SET $kit_number_map("%number%")=number_new
			OUTER_SPRINT kitlist_write "%kitlist_write%%number_new%%TAB%%data%%WNL%"
			OUTER_SET number_new=number_new+1
		END
	END
	
	// put the multi-class data back

	ACTION_PHP_EACH kitlist_contents AS number=>data BEGIN
		ACTION_IF VARIABLE_IS_SET $is_multiclass("%number%") BEGIN //multiclass kit
			OUTER_SET $kit_number_map("%number%")=number_new
			OUTER_SPRINT kitlist_write "%kitlist_write%%number_new%%TAB%%data%%WNL%"
			OUTER_SET number_new=number_new+1
		END
	END	
	
	// write the data to kitlist
	
	APPEND "kitlist.2da" "%kitlist_write%"
	COPY_EXISTING "kitlist.2da" override PRETTY_PRINT_2DA
	
	// update the k_x_y entries
	
	COPY_EXISTING "kittable.2da" override
		COUNT_2DA_COLS colcount
		READ_2DA_ENTRIES_NOW dw_kittable_data colcount
		FOR (row=0;row<dw_kittable_data;++row) BEGIN
			FOR (col=1;col<colcount;++col) BEGIN
				READ_2DA_ENTRY_FORMER dw_kittable_data row col file
				INNER_ACTION BEGIN
					LAF remap_k_x_y STR_VAR file END
				END
			END
		END	
	BUT_ONLY
	

END

DEFINE_PATCH_FUNCTION class_is_multiclass STR_VAR class_id=0 RET multiclass
BEGIN
	PATCH_MATCH "%class_id%" WITH
	7 8 9 10 13 14 15 16 17 18 BEGIN
		SET multiclass=1
	END
	DEFAULT
		SET multiclass=0
	END
END

DEFINE_ACTION_FUNCTION remap_k_x_y STR_VAR file="" BEGIN

	ACTION_IF FILE_EXISTS_IN_GAME "%file%.2da" BEGIN
		COPY_EXISTING "%file%.2da" override
			COUNT_2DA_ROWS 2 rowcount
			FOR (row=0;row<rowcount;++row) BEGIN
				READ_2DA_ENTRY row 1 2 number
				PATCH_IF IS_AN_INT number BEGIN
					SET new_number=$kit_number_map("%number%")
					SET_2DA_ENTRY row 1 2 new_number
				END
			END
			PRETTY_PRINT_2DA
		BUT_ONLY
	
	END

END

 

Link to comment

That's one of the things I was interested in advice about. I think that the actual row number in kitlist.2da turns up in game in very few places. CRE files put the KITIDS entry from kitlist.2da into the kit field, not the row number. Likewise, opcodes 177, 318, 324, 326 check the ids number, So do BCS triggers. And of course rearranging the order of entries in kitlist.2da doesn't change which ids entry is associated with which kit. CLAB files appear directly in kitlist.2da and don't need looking up. LUA entries are from LUABBR, which looks up by rowname, not rownumber. Proficiencies are indexed directly via the proficiency entry in kitlist. And so on.

The k_x_y 2das that associate kits to race/class combos do a direct lookup via kitlist rownumber. So far as I can tell that's the only place it's used. (And my code above updates them.)

But: I admit freely that this is not my main area of expertise. If I'm missing something, I'd be glad to be told.

 

Link to comment
32 minutes ago, DavidW said:

Proficiencies are indexed directly via the proficiency entry in kitlist. And so on.

This probably could stand to be tested. I’m not sure whether the Proficiency value in kitlist determines which column is used by that kit, or whether it is simply for easy lookups. But it’s probably fine. 

That code will probably work fine. I only don’t have a situation in mind where it would be used. But presumably you do. (Adding kits for enemies in SCS? Doing an actual PC-focused kit mod? If the latter, you might consider just keeping it separate from SCS. This code might solve the kit limit problem, but keeping the hypothetical mod separate would maximize players’ ability to easily control their install order. (Or maybe the goal is to liberalize the options available in the NPC Customization component. I’ve long back-burnered an idea to clone every kit to every valid class, as an option in my NPC customization mod. But that would mostly mean making multiclass copies of single-class kits, so it wouldn’t implicate the kit limit.))

Link to comment
9 hours ago, DavidW said:

That's one of the things I was interested in advice about. I think that the actual row number in kitlist.2da turns up in game in very few places. CRE files put the KITIDS entry from kitlist.2da into the kit field, not the row number. Likewise, opcodes 177, 318, 324, 326 check the ids number, So do BCS triggers. And of course rearranging the order of entries in kitlist.2da doesn't change which ids entry is associated with which kit. CLAB files appear directly in kitlist.2da and don't need looking up. LUA entries are from LUABBR, which looks up by rowname, not rownumber. Proficiencies are indexed directly via the proficiency entry in kitlist. And so on.

The k_x_y 2das that associate kits to race/class combos do a direct lookup via kitlist rownumber. So far as I can tell that's the only place it's used. (And my code above updates them.)

But: I admit freely that this is not my main area of expertise. If I'm missing something, I'd be glad to be told.

I think the KITIDS column in kitlist.2da must correspond to the row number, but I have to do some more tests to be certain. A quick test showed that character creation can't be completed if row number and the lowest order byte of the KITIDS value are different. Exceptions are specialist mages and the barbarian kit.

If KITIDS is indeed linked to the kitlist row number then you'd have to update a lot of game resources.

Link to comment

After more testing it looks like KITIDS values don't have to correspond to the kitlist row number as long as a valid kit entry with the row number equal to the lowest order byte value of KITIDS exists. Otherwise character creation fails to list available alignment entries for the selected kit.

So in theory sorting the kitlist table works without unwanted side effects, but it would be a rather fragile solution.

Edited by argent77
Link to comment

That's matching some of what I've just been finding. I got through character creation ok but was getting alignment anomalies. Will test a bit more, if only for my own curiosity. It offends my mathematical sensibilities to have arbitrary rules like this...

EDIT: If I understand that correctly, won't it always be satisfied? I'm only permuting table entries, not deleting any. (But then, in that case I shouldn't have got alignment anomalies on my own run.

Link to comment
17 minutes ago, DavidW said:

Actually @argent77: when you say it 'fails to list available alignment entries' do you mean it doesn't list them at all, or that it gets them wrong?

Yes, no entries at all. All you can do is go back and select another kit that works.

As I said, in theory your idea should work since it only permutes existing rows. But you don't know if mods installed afterwards might get confused because of the mismatching row number / kitids values. There could also be other side effects that we haven't yet noticed.

Link to comment

I'm finding that the alignment comes back wrong even if you just permute rows.

... I'm basically concluding that this is more trouble than it's worth. The game is doing some weird lookup stuff that doesn't really track the implied logic of its tables, and the risk of breaking something is probably too high. Thanks for your help with it.

Link to comment

Join the conversation

You are posting as a guest. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...