Jump to content

functions to allow multiple mods to edit HLA tables


subtledoctor

Recommended Posts

UPDATE: as described later on in the thread, I have written up some functions and posted them for anyone to use. Here you go:

Modify_HLAs Weidu

 

Preamble: HLA modding is a pain. It's easy to mod class and kit abilities - you can just APPEND stuff to the clab tables. But HLA tables are different, they all contain exactly 24 rows, of which only some are filled. Existing mods that change HLAs all simply overwrite the tables. Which leads to hard incompatibility between those mods. (Consider Refinements vs. Rogue Rebalancing.)

 

It's possible to use READ_2DA_ENTRY to find and fill the next available slot, but that's a bit complicated and it would be difficult for a mod to have to figure out how many free rows there are and tailor its installation to that number. Not for nothing, there isn't a single mod out there that uses this technique.

 

But I may have hit on a middle way. I've created a system that dynamically builds custom HLA tables for each priest kit, based on their sphere access. Here's what I've found (my testing has been on EE 1.3 and 2.0 systems... I haven't tested this on BGT but it's probably worth it):

- HLA tables don't need to be 24 rows long. You can remove some rows, or add some rows, it doesn't matter, the game presents the table to you without complaint.

- In EE 2.0+ there are scroll bars so HLA tables can contain more than 25 HLAs. I seem to recall that ToBEx added these scroll bars as well... but I'm not certain. (Anyone know?)

 

My mod works by starting with a base HLA table that has no 'empty' rows at the bottom of it. It then checks for which spheres the cleric has "major" access to, and appends rows to the table which contain the HLAs for each of those sphere. Finally, it appends some vanilla-style blank rows at the bottom. (I suspect this step is unnecessary.) After that's done, some kits' HLA tables are only 17 rows long, and others are 30 rows long. But the game doesn't mind.

 

Conceptually, I can envision how different mods might use a uniform system to work together. The steps would be something like:

 

1) Analyze the HLA table, find the first row (if any) that is blank. Delete this row and all subsequent rows.

 

2) Now left with a table that only contains rows for actual HLAs, the mod can append new rows to add custom ones.

 

3) Optionally, you can append some blank lines to make the table look nice.

 

Step 1 is really the only difficult part, as a matter of coding. My 2da-parsing skills are not up to snuff. But if we can figure it out, we could make the code usable by all mods, big and small, and any modder could feel more comfortable adding HLAs without the burden of having to replace a whole table.

 

Any thoughts? Anyone good at 2da hacking want to take a stab at step 1? If we can figure it out, I'll write up a sample mod that others can adapt in their own mods...

Link to comment

1) Just go down the lines, read if the ability row is *, then the entire line can be "removed" ... or more, replaced, when there's a need.

 

I never needed to really have more than 24 HLAs, but having the option to have more is nice. Be sure to edit the Fighter/Mage-or-Cleric/Thief HLA tables to have all the mages/clerics abilities and not just the bum Fighter & Thief ones.

There was more than 24 ability option in ToBEx, but it was broken in the first release, and that was all I heard from it.

Link to comment

this is what functions and macros are for, so people don't need to start from scratch. But just appending extra lines means all the changes are cumulative, so mods won't be able to disable each other (think for example of the scs and kr/sr interplay).

Link to comment

Hacked this together on my way home for practice. Is this what you were looking for?

COPY_EXISTING ~luth0.2da~ ~override/testremoverows.2da~
  COUNT_2DA_COLS cols // amount of columns
  COUNT_2DA_ROWS cols rows // amount of rows
  READ_2DA_ENTRIES_NOW file cols // read all file into memory
  
  first_empty_row = rows // default value to amount of rows in order to skip removal if the table is full
 
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    SET empty_col_count = 0 // amount of empty columns in the row
    FOR (j = 0; j < cols; ++j) BEGIN // iterate over columns in the row
      READ_2DA_ENTRY_FORMER file i j col_value // read column value
      PATCH_IF "%col_value%" STRING_EQUAL "*" BEGIN // asterisk symbolizes empty row
        empty_col_count += 1
      END
    END
    
    PATCH_IF "%empty_col_count%" = ("%cols%" - 1) BEGIN // first column in every row is its number, that's why (cols - 1)
      first_empty_row = i // remember the first empty row
      i = file // skip iterating over the rest of the rows
    END
  END
  
  WHILE ("%first_empty_row%" < "%rows%") BEGIN  // iterate over rows to remove
    REMOVE_2DA_ROW j cols // kill the row
    rows = rows - 1 // decrease the total amount of rows
  END
luth0.2da:

2DA V1.0
*
        ABILITY       ICON        STRREF    MIN_LEV   MAX_LEVEL  NUM_ALLOWED  PREREQUISITE EXCLUDED_BY   ALIGNMENT_RESTRICT
1       GA_SPCL910     *          *         1         99         16           *            *             *
2       GA_SPCL911     *          *         1         99         16           *            *             *
3       GA_SPCL912     *          *         1         99         16           *            *             *
4       GA_SPCL913     *          *         1         99         16           *            *             *
5       GA_SPCL914     *          *         1         99         16           GA_SPCL913   *             *
6       AP_SPCL915     *          *         1         99         1            *            *             *
7       GA_SPCL916     *          *         1         99         16           *            GA_SPCL916    *
8       GA_SPCL917     *          *         1         99         16           *            *             *
9       GA_SPCL918     *          *         1         99         16           *            *             *
10      GA_SPCL919     *          *         1         99         16           AP_SPCL915   *             *
11         *           *          *         *         *          *            *            *             *
12         *           *          *         *         *          *            *            *             *
13         *           *          *         *         *          *            *            *             *
14         *           *          *         *         *          *            *            *             *
15         *           *          *         *         *          *            *            *             *
16         *           *          *         *         *          *            *            *             *
17         *           *          *         *         *          *            *            *             *
18         *           *          *         *         *          *            *            *             *
19         *           *          *         *         *          *            *            *             *
20         *           *          *         *         *          *            *            *             *
21         *           *          *         *         *          *            *            *             *
22         *           *          *         *         *          *            *            *             *
23         *           *          *         *         *          *            *            *             *
24         *           *          *         *         *          *            *            *             *
test.2da:

2DA V1.0
*
        ABILITY       ICON        STRREF    MIN_LEV   MAX_LEVEL  NUM_ALLOWED  PREREQUISITE EXCLUDED_BY   ALIGNMENT_RESTRICT
1       GA_SPCL910     *          *         1         99         16           *            *             *
2       GA_SPCL911     *          *         1         99         16           *            *             *
3       GA_SPCL912     *          *         1         99         16           *            *             *
4       GA_SPCL913     *          *         1         99         16           *            *             *
5       GA_SPCL914     *          *         1         99         16           GA_SPCL913   *             *
6       AP_SPCL915     *          *         1         99         1            *            *             *
7       GA_SPCL916     *          *         1         99         16           *            GA_SPCL916    *
8       GA_SPCL917     *          *         1         99         16           *            *             *
9       GA_SPCL918     *          *         1         99         16           *            *             *
10      GA_SPCL919     *          *         1         99         16           AP_SPCL915   *             *

 

Link to comment

Genius! I never noticed that there is a "remove_2da_row" command... :p

 

@Jarno the point is not so that we can have more and more HLAs, but just so that different mods can be compatible.

 

For instance, take Rogue Rebalancing which overhauls all of the thief HLAs. Say a small-time modder has a brilliant idea for ONE awesome HLA for thieves, but doesn't want to overhaul the whole HLA system. He can add his HLA in his mod... but currently, he would have to include a disclaimer in his read me "this is not compatible with RR HLAs." Who wants to do that? It's a huge disincentive to make any mods that affect HLAs.

 

Whereas, if we can make a macro to patch HLA tables in a non-destructive fashion, and RR and small-time-modder both use it, then players can feel free to use both mods, or either one, or neither, without any issues.

 

Consider also Refinements vs. RR. These HLA tables are actually pretty similar, I think they include some of the same abilities. By trimming the table and appending rows to it individually, it would be super-easy to add a check in Refinements that says "if Magic Flute is already in the table, then don't add our version of it." Or, alternatively, "if Magic Flute is already in the table, then replace it with our version instead of appending our version."

 

Suddenly a bunch of existing mod conflicts will just disappear, and new modders will feel encouraged to experiment in this area.

 

Lynx, I'm not sure how SCS/KR/SR disable each other, but using this method it would be pretty simple to look for and remove things from the table, when needed. (E.g. my upcoming mod will add Use Scrolls as a low-level feat for thieves, so I will remove the Refinements version of it from the HLA table.)

Link to comment

So, maybe define a patch function that accepts as STR_VAR variables values that can be used in each column of the table, and the function would perform critto's trimming code above, then string the variables together and append them to the table. So then for modders, adding HLAs would be as easy as:

COPY_EXISTING ~lufi02.2da~ ~override~
   LPF add_hla BEGIN
      STR_VAR spl = ~d5_myhla~ min_level = ~1 ~max_level = ~99~ num_allowed = ~3~
   END
   PRETTY_PRINT_2DA
That would involve adding HLAs one at a time, which could be a pain, but it's a lot better than the way things are now!

 

EDIT - thinking about this more, it would be ideal to have a suite of functions. In addition to ADD_HLA, consider a situation like Faiths & Powers, which currently overwrites the base cleric HLA table. What if some small mod uses ADD_HLA before F&P is installed? It would be wiped out. Since F&P specifically wants to remove several vanilla HLAs from the table, it would be better off using a targeted REMOVE_HLA function. Then the vanilla spells would be eliminated but the mod-added one would remain.

 

And what about a situation like Refinements, which eliminates Use Any Item from thieves and instead gives them Use Scrolls? This is calling out for a REPLACE_HLA function.

 

The vanilla HLA system is just so awful it's not even funny. (Bards suddenly setting traps like a master thief? Okay) I think something like this could really unleash peoples' creativity to make that system better.

Link to comment

Is there really no patch method to APPEND text?? Other than APPEND_FILE?

 

EDIT - Oops, just noticed INSERT_2DA_ROW. Never mind me.

 

EDIT 2 - Sweet, this was all much easier than I thought. Here's a first pass:

 

https://github.com/subtledoctor/Modify_HLAs

 

It includes an example mod. To use it elsewhere, you can grab the hla_actions.tpa file from mod_hlas/lib/ and INCLUDE it in your own mod. How to use:

 

The add_hla function takes STR_VAR variables matching the columns in HLA .2da tables. The default value for each one is ~*~ except "min_lev" and "max_level" which are ~1~ and ~99~ respectively. You only need to define variables for columns you want to change. Here's my example code giving Tracking and Summon Deva to trueclass fighters:

COPY_EXISTING ~lufi0.2da~ ~override~
    LPF add_hla STR_VAR ability = ~GA_SPCL922~ num_allowed = ~1~ excluded_by = ~GA_SPCL922~ END
    LPF add_hla STR_VAR ability = ~GA_SPCL923~ num_allowed = ~20~ END
BUT_ONLY

The remove_hla function is simpler, it only takes a single variable: the .SPL filename of the HLA you want to remove. Here I reduce the HLAs available to clerics and mages:

COPY_EXISTING ~lucl0.2da~ ~override~
    LPF remove_hla STR_VAR ability = ~SPPR723~ END
BUT_ONLY

COPY_EXISTING ~luma0.2da~ ~override~
    LPF remove_hla STR_VAR ability = ~SPCL930~ END
BUT_ONLY

I was going to write up a replace_hla function, but it's not really necessary, mods can simply remove_hla and then add_hla (or vie versa).

 

Huge thanks to critto, this is like 90% him and 10% me... :thumbsup:

 

Now, I'm off to incorporate this into refinements...

Link to comment
I was going to write up a replace_hla function, but it's not really necessary, mods can simply remove_hla and then add_hla (or vie versa).

You can still add replace_hla and have it call both remove_hla and add_hla. But that's just for convenience, nothing much else.

 

https://github.com/subtledoctor/Modify_HLAs/blob/master/mod_hlas/lib/hla_actions.tpa#L23 this line has a wrong comment, I meant to say "empty column"

 

Also, re-check remove_hla carefully. I have a suspicion that this style of iteration over rows in 2DA might cause you to skip over rows because the total amount of rows in file decreases.

Link to comment

Also, re-check remove_hla carefully. I have a suspicion that this style of iteration over rows in 2DA might cause you to skip over rows because the total amount of rows in file decreases.

Theoretically, the iteration should be fine (since all the variables are set when READ_2DA_ENTRIES_NOW is performed), but if the loop encounters more than 1 row to remove, the index will no longer be accurate.

 

I think something like this might solve it (not tested).

  COUNT_2DA_COLS cols // amount of columns
  COUNT_2DA_ROWS cols rows // amount of rows
  READ_2DA_ENTRIES_NOW file cols // read all file into memory
  SET num_deleted = 0
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    READ_2DA_ENTRY_FORMER file i 1 col_value // read column value
    PATCH_IF "%col_value%" STRING_EQUAL "GA_%ability%" OR "%col_value%" STRING_EQUAL "AP_%ability%" BEGIN // match .spl to be removed
      REMOVE_2DA_ROW (i - num_deleted) cols // kill the row
      SET num_deleted += 1
    END
  END

On another note, I think it might make sense to require callers of remove_hla to specify the ability with the GA_ or AP_ prefix included, just so it matches the formatting of add_hla.

Link to comment

Ah - good point. I didn't envision a circumstance where an ability would appear more than once in a table. But I suppose it can happen. (In fact I think my old mod use to do to replace the two Elemental Transformation abilities with two instances of a Werewolf Lord shapechange, for Shapeshifters... back then I was stuck using REPLACE_TEXTUALLY but this function would work much better.)

On the GA_ and AP_ in remove_hla... I went back and forth on that. I think it's easier for someone to go through the mental process
- I want to remove Summon Deva
- find Summon Deva... okay it's SPCL723
- remove any instance of SPCL723


On the other hand, uniformity is probably more important. If someone gets used to leaving out the GA_ in remove-hla, and then they decide to use add_hla, they might forget the usage is different and only put the .SPL name, and then it will fail and they come to the forums to ask why. :p

Another alternative is to add a variable to add_hla that accepts either ~GA~ or ~AP~ and let the modder define it that way. I think this is actually the most user-friendly, but I don't like that it eliminates the consistency of having one variable for every column of the .2da table.

You can still add replace_hla and have it call both remove_hla and add_hla. But that's just for convenience, nothing much else.


I was going to, but then I thought about how users might like to use it, and if they want to remove 2 HLAs and add 1, or remove 1 and add 2, etc., the function wouldn't be able to handle that and combing replace_ with one of the others might add mental complexity to the task. Keeping them separate sacrifices a bit of convenience, but maintains clarity/simplicity.

 

But here too, I could be convinced otherwise. As a matter of implementation, I'm pretty sure it's just a matter of stacking the two functions together. (The row count would change after a remove_ occurs, so the best way to stack them would be to run an add_, then run a remove_.)

 

Btw, I've tested these on BGT (with ToBEx - I don't have a clean install without ToBEx handy) and the resulting fighter table, with only 11 rows of HLAs and no blank rows after them, works perfectly well. The add_hla in the example mod allows fighters to choose Tracking and Summon Deva. :) Most vanilla HLA tables are pretty short, with only 10 or so entries, so hopefully modders will get creative and add a bunch of cool stuff.

Link to comment
I was going to, but then I thought about how users might like to use it, and if they want to remove 2 HLAs and add 1, or remove 1 and add 2, etc., the function wouldn't be able to handle that and combing replace_ with one of the others might add mental complexity to the task. Keeping them separate sacrifices a bit of convenience, but maintains clarity/simplicity.

I am not convinced. Maybe it's just because I'm used to writing code, but I don't see a problem using WeiDU's replace_item where it fits although I have delete_item and add_item in my arsenal as well (and I do use them too when the need arises).

 

 

 

Ah - good point. I didn't envision a circumstance where an ability would appear more than once in a table. But I suppose it can happen.

It can and will happen. Lots of code written by different people, many of whom aren't used to thinking in terms of efficiency and interoperability when it comes to modding (and why would they? they are making a mod for a game), issues like this are bound to come up. Better be prepared for them in advance.

 

 

Another alternative is to add a variable to add_hla that accepts either ~GA~ or ~AP~ and let the modder define it that way. I think this is actually the most user-friendly, but I don't like that it eliminates the consistency of having one variable for every column of the .2da table

 

 

Generally, I don't see an issue with another variable to clarify the purpose. Static definition of types leaves less room for error and incorrect interpretation. When people are forced to think more of the stuff they write, they tend to make less errors. Also, if you implement replace_hla, you need provide variables that define types both for replaceable element and its replacement. Nothing's preventing you or anyone from replacing GA_SPL1 with AP_SPL2.

Link to comment

Okay, I've incorporated those suggestions into build 0.2:

 

https://github.com/subtledoctor/Modify_HLAs/releases/tag/0.2

 

Changes:

- The add_hla function is unchanged.

- The remove_hla function now uses %remove_ability% as its variable, and you must include the full phrase, i.e. "GA_SPCL923" or AP_SPCL918"

- I've added a replace_hla function which uses all of the variables of the other two functions, i.e. every column in an HLA table plus %remove_ability%

- It occurred to me that, since these use INSERT_2DA_ROW instead of APPEND, there is no need to strip out the blank rows at the bottom of the file. So that doesn't happen anymore.

 

The one tool I would like to add to this is an action function for GET_HLA_TABLE, analogous to RES_NUM_OF_SPELL_NAME... so the modder can enter the IDS name of a kit and the function will spit out the HLA table from LUABBR.2da.

 

The use case I'm envisioning is this: say I want to add some cool HLAs specifically for the Bounty Hunter, like a Web Trap. In vanilla tha BH uses luth0 just like the trueclass thief. So I need to copy over a custom table - call it "lud5bh.2da" - and put it in luabbr.2da next to the Bounty Hunter.

 

But what if Refinements has already been installed? Then the BH will be using luth3.2da as its table, and my custom table shouldn't replace it, but should add to it instead. So, it would be nice to do something like this:

LAF get_hla_table STR_VAR kit = ~bounty_hunter~ RET = ~hla_table~ END

ACTION_IF %hla_table% STRING_EQUAL_CASE ~th0~ BEGIN
   COPY ~mymod/lud5bh.2da~ ~override~
   COPY_EXISTING ~luabbr.2da~ ~override~
      SET_2DA_ENTRY yadda yadda 
END
ELSE BEGIN 
   COPY_EXISTING lu%hla_table%.2da~ ~override~
      LPF add_hla STR_VAR ability = yadda yadda
Link to comment

Okay, that turned out to be pretty easy as well. A productive lunch hour! Here is 0.3 with an added function, "get_hla_table"

 

https://github.com/subtledoctor/Modify_HLAs/releases/tag/0.3

 

Anyone want to give this a whirl? I'm about ready to advertise this and tell people to start making tons of HLAs. But there are probably issues I'm not seeing. Should anything be added to it? Fixed? Streamlined? Etc.

 

(For ease of reference, here is the code in its entirety:

 

 

 

DEFINE_PATCH_FUNCTION add_hla
  STR_VAR
    2da_row = ~1~
    ability = ~*~
    icon = ~*~
    strref = ~*~
    min_lev = ~1~
    max_level = ~99~
    num_allowed = ~*~
    prerequisite = ~*~
    excluded_by = ~*~
    alignment_restrict = ~*~
BEGIN
  COUNT_2DA_COLS cols // amount of columns
  COUNT_2DA_ROWS cols rows // amount of rows
  READ_2DA_ENTRIES_NOW file cols // read all file into memory  
  first_empty_row = rows // default value to amount of rows in order to skip removal if the table is full
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    SET empty_col_count = 0 // amount of empty columns in the row
    FOR (j = 0; j < cols; ++j) BEGIN // iterate over columns in the row
      READ_2DA_ENTRY_FORMER file i j col_value // read column value
      PATCH_IF "%col_value%" STRING_EQUAL "*" BEGIN // asterisk symbolizes empty column
        empty_col_count += 1
      END
    END
    PATCH_IF "%empty_col_count%" = ("%cols%" - 1) BEGIN // first column in every row is its number, that's why (cols - 1)
      first_empty_row = i // remember the first empty row
      i = file // skip iterating over the rest of the rows
    END
  END  
  INSERT_2DA_ROW ("%first_empty_row%") %cols% ~%2da_row% %ability% %icon% %strref% %min_lev% %max_level% %num_allowed% %prerequisite% %excluded_by% %alignment_restrict%~
  PRETTY_PRINT_2DA
END


DEFINE_PATCH_FUNCTION remove_hla
  STR_VAR
    remove_ability = ~*~
BEGIN
  COUNT_2DA_COLS cols // amount of columns
  COUNT_2DA_ROWS cols rows // amount of rows
  READ_2DA_ENTRIES_NOW file cols // read all file into memory
  SET num_deleted = 0
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    READ_2DA_ENTRY_FORMER file i 1 col_value // read column value
    PATCH_IF "%col_value%" STRING_EQUAL_CASE "%remove_ability%" BEGIN // match .spl to be removed
      REMOVE_2DA_ROW (i - num_deleted) cols // kill the row
      SET num_deleted += 1
    END
  END
END


DEFINE_PATCH_FUNCTION replace_hla
  STR_VAR
    remove_ability = ~*~
    2da_row = ~1~
    ability = ~*~
    icon = ~*~
    strref = ~*~
    min_lev = ~1~
    max_level = ~99~
    num_allowed = ~*~
    prerequisite = ~*~
    excluded_by = ~*~
    alignment_restrict = ~*~
BEGIN
  COUNT_2DA_COLS cols // amount of columns
  COUNT_2DA_ROWS cols rows // amount of rows
  READ_2DA_ENTRIES_NOW file cols // read all file into memory  
  SET num_deleted = 0
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    READ_2DA_ENTRY_FORMER file i 1 col_value // read column value
    PATCH_IF "%col_value%" STRING_EQUAL_CASE "%remove_ability%" BEGIN // match .spl to be removed
      REMOVE_2DA_ROW (i - num_deleted) cols // kill the row
      SET num_deleted += 1
    END
  END
  first_empty_row = rows // default value to amount of rows in order to skip removal if the table is full
  FOR (i = 0; i < file; ++i) BEGIN // iterate over rows
    SET empty_col_count = 0 // amount of empty columns in the row
    FOR (j = 0; j < cols; ++j) BEGIN // iterate over columns in the row
      READ_2DA_ENTRY_FORMER file i j col_value // read column value
      PATCH_IF "%col_value%" STRING_EQUAL "*" BEGIN // asterisk symbolizes empty column
        empty_col_count += 1
      END
    END
    PATCH_IF "%empty_col_count%" = ("%cols%" - 1) BEGIN // first column in every row is its number, that's why (cols - 1)
      first_empty_row = i // remember the first empty row
      i = file // skip iterating over the rest of the rows
    END
  END  
  INSERT_2DA_ROW ("%first_empty_row%" - 1) %cols% ~%2da_row% %ability% %icon% %strref% %min_lev% %max_level% %num_allowed% %prerequisite% %excluded_by% %alignment_restrict%~
  PRETTY_PRINT_2DA
END


DEFINE_ACTION_FUNCTION get_hla_table
  STR_VAR
    kit_name = ~~
  RET
    hla_table
BEGIN
  COPY_EXISTING ~luabbr.2da~ ~override~
    READ_2DA_ENTRIES_NOW file 2 // read all file into memory
    FOR (row = 1; row < file; row += 1) BEGIN // iterate over rows
      READ_2DA_ENTRY_FORMER ~file~ row 0 kit
      READ_2DA_ENTRY_FORMER ~file~ row 1 table
      TEXT_SPRINT $d5_hla_tables(~%kit%~) ~%table%~
    END
  BUT_ONLY
  ACTION_PHP_EACH d5_hla_tables AS ind => tab BEGIN
    ACTION_IF ~%ind%~ STRING_EQUAL_CASE ~%kit_name%~ BEGIN
      OUTER_TEXT_SPRINT hla_table ~%tab%~
    END
  END
END

Link to comment

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...