Jump to content

DavidW

Gibberlings
  • Posts

    8,048
  • Joined

  • Last visited

Posts posted by DavidW

  1. 5 hours ago, Luke said:

    we should also take into account all those items/spells that "drain" attributes via something like op44 – Strength bonus (f.i. "shadowwp.itm")...

    I was being selective given that splstate slots are an at-least-somewhat-scarce resource.(I did an automated trawl through all the 101 effects in BG2EE.) In this case, only one spell in BG2 gives immunity to op44, and its existing implementation looks fine without a spellstate.

    13 hours ago, DavidW said:

    I am actually tempted - there are significant AI advantages

    On reflection, they're not as good as I thought. SCS needs to distinguish between protections that are immediately obvious (e.g. don't charm undead) and protections that you can only discover through trial and error (e.g. don't charm people wearing Helms of Charm Protection). So I take back the 'AI advantages' point, although I'm still attracted to the overall elegance of the scheme.

     

    12 hours ago, subtledoctor said:

    400 more spellstates. I know it's black magic but... it works.

    That's incredibly clever. I think I agree that it's too hacky to use in a FP, but the very fact that it can be done takes the pressure of splstate.2da and makes me somewhat more relaxed about grabbing a bunch of spellstates for FP.

  2. Doing a quick automated audit in BG2, I think there would indeed need to be about 20 spellstates to implement that method of doing effect blocking:

    charm (6)
    haste(16)
    fear (23,24,106)
    unconscious (39,217)
    slow (40,210)
    stun (45)
    death (55,206)
    blindness (74)
    feeblemind (76)
    disease (78)
    deafness (80)
    fatigue (93)
    hold (109,175,185)
    confusion (128)
    petrification (134)
    polymorph (135)
    imprisonment (211)
    maze (213)
    energy drain (216)
    disintegrate (238)

    I am actually tempted - there are significant AI advantages - but it does rather depend on how much mods are using up splstate.ids - cf my other thread.

  3. It would be good to know how severe the demands on spellstates actually are. There are 256 slots available, of which the vanilla game uses 133, leaving 123 free. My mods (mostly SCS, but also some upcoming projects) use maybe 30-40 of those. Does anyone have an impression of what other mods do? If actually the extant mods only use 50-60 in total, we could borrow a bunch for FP with impunity (which would simplify immunities); if there's real pressure on splstate.2da, of course we shouldn't.

    (It would be *really* nice if BD were willing to extend splstate to 2 bits in 2.7)

  4. I think there are two factors here.

    (1) What feature of a creature actually codes its immunity to some effect?
    (2) Given an answer to (1), how do we implement that immunity?

    I'll use charm immunity for undead as an example. In BG2, what makes an undead creature immune to Charm is that it has ring95. In IWD, what makes it immune is that it has GENERAL=UNDEAD. In other words, in IWD it's immune simply because it's undead; in BG2 the fact that it's undead only matters through the developer's decision to give the creature ring95. Internal to the logic of BG2, undead aren't charm-proofed; only ring95-holders are.

    As I said in the earlier mail thread, these are two fundamentally different choices of immunity architecture, embedded *very* deeply into the respective games. There are defensible arguments for either, but I think it's a really bad idea to try rewriting the architecture of any of the games to this degree. I suspect Cam's compelling example of the Chromatic Demon would just be the tip of the iceberg.

    But even holding the architecture fixed, there are still substantive questions about how it is implemented. In the IWd architecture, there's not much choice: there is no way to attach effects to a RACE or GENERAL setting, so the only available implementation is to tell each appropriate spell and item to ignore creatures of that RACE or GENERAL, using 318/324. This is basically what IWDEE already does.

    It's more complicated on the BG2 architecture. There are 3 options:

    (i) Use 101 on the immunity item (ring95 in our example) to block the core effect, and then add a bunch of string/animation immunities to block associated strings/animations.
    (ii) For each spell/effect that implements the blocked effect (e.g. Charm), add that spell to the immunity item via 206. (Or 318; I don't think it matters.)
    (iii) Get the immunity item to set a spellstate (say, BLOCKS_CHARM), and then edit all spells/items that inflict the blocked event so as to add a 318/324 block keyed to that spellstate.

    Importantly, these are not mutually exclusive.

    I'm going to argue that we should always *at least* use (i) when it's viable. The absolutely-most-important feature of an immunity-to-charm option is that it blocks the charm opcodes. Secondarily, it needs to block the clearly-visible signatures of charm: the displayed 'Charmed'/'Dominated' strings, the icons, and the SPNWCHRM animation. Blocking other effects (e.g. sounds) is a very distant third. It is way too dangerous to rely on (ii) and (iii) to block the charm opcodes: even in the unmodded game I'd be nervous, but mods add vast numbers of new items and spells and there is no way to enforce that those mod items/spells add 206 to the immunity items *or* 318/324 to themselves. Blocking the relevant opcodes direct is the only safe option.

    BUT there is a perfectly good case for ALSO blocking the core spells directly via either (ii) or (iii): it addresses the sound-effect issue and any other stray effects, and it's harmless to double up. In principle I think (iii) (new spellstate set by the immunity item, spells detect the spellstate and self-block) is better: it's more elegant, clutters up the spell less, and is resilient against mods that clone the spell. However, the cost in spellstates is way too high: we'd need 20 or more. So (reluctantly!) I think in practice using 206 on the immunity item is better.

    However, for immunity *spells* there is an spellstate available already: the spellstate set by DS. There is no reason why, e.g., Charm Person couldn't just self-block if it detects the CHAOTIC_COMMANDS spellstate. (We can't, unfortunately, add that spellstate to ring95, because both SoD and mods like SCS assume that CHAOTIC_COMMANDS marks the specific - and dispellable/breachable - spell, not just generic immunity.) I think that move is more elegant for spells than adding a bunch of 206s to the spell (though I'm not super-sold on this.)

    Cam raises two special cases - Cloudkill and Song of Kaudies - but I think they fit in this general logical framework. The underlying issue is that the game contains no resource to designate a spell as Poison / Sound. So the relevant effect needs to sit on the spell itself: that's much cleaner logic than just calling out the spells to each relevant item. Doing so for Cloudkill, as Cam says, is easy: just do a 318/324 keyed to poison resistance. I agree that Sound needs a new spellstate, IMMUNE_TO_SOUND; then what marks something as a Sound effect is just that it does a 318/324 self-block if it runs into IMMUNE_TO_SOUND.  I think this is a satisfactory reason to use up one spellstate slot; in general I think using up the odd one is fine, I just would want to resist a systematic policy that uses up dozens. 

    Summary:

    - For IWDEE creature immunities, I don't think we should do anything
    - For BG(2)EE creature immunities, we should maintain the existing 101 blocks but should also add a 206 block to probably any vanilla-game resource; certainly any vanilla-game resource with sound effects.
    - For spells that grant immunities, we should use their DS spellstates/stats to directly block any vanilla-game resource (or, again, any vanilla-game resource with a sound effect). That means that we should after all include basic DS in the fixpack. 
    - We should follow Cam's suggestions for Cloudkill and Song of Kaudies.

  5. I am very pro following the same framework as the BG2 fixpack. I don't think there is any better standard available for bugs than developer intent, and I think the BG2 fixpack both interpreted what that means sensibly and sets a good precedent.

    1 hour ago, Luke said:
    10 hours ago, CamDawg said:

    IDS/2DA fixes

    • Syntax errors

    So this includes fixing things like

    KIT.IDS => ASSASIN // should be ASSASSIN (two S)
    GENDER.IDS => NIETHER // should be NEITHER

    ...?

    But that would require adjusting scripts/dialogs as well...

    These aren't syntax errors. Assigning 'ASSASIN' rather than 'ASSASSIN' as the ids name for kit 0x400a is a dumb choice and presumably happened because someone at Bioware couldn't spell 'assassin', but it's perfectly legal syntax and doesn't need changing. (And doing so would break dozens of mods.)

  6. 18 minutes ago, subtledoctor said:

    I see no problem with simply removing all +1 weapons from the game...

    In BG1 I think it serves a purpose to leave a few around, given that there are some monsters that can't be harmed by nonmagical weapons (and given that there are few enough magic weapons that finding one is cool).

  7. SCS does exactly what your * suggests, i.e. the default magic +1 weapons become high-quality, but there's copy made of each of them and the copies are placed in hand-picked locations.

    (Well, that's what happens in BG1. In BG2 I don't bother with the hand-picked placements since there are so many +2 and better items anyway.)

  8. 11 minutes ago, CamDawg said:

    This is fixed, kinda. In oBG bastard swords, scimitars, long swords, and two-handed swords all use the same item type. (In BG2, we just use proficiency which avoids this altogether.) Two-handed swords get filtered out by checking if the weapon is one-handed, and (as of v10) long swords and scimitars get filtered out by checking for usability of single-class druids and thieves. This still isn't perfect--if a mod adds a scimitar or long sword that's unusable by thieves and druids, it'll get picked up as a bastard sword. I feel like the other things that we could check--base damage, minimum strength, etc.--are less reliable than usabilities, but I'm open to suggestions.

    Can you check the unidentified name? Most magic items will have the name of the nonmagical version in that field. (If they don't have a recognized unidentifiable name, you could use a secondary heuristic.)

  9. Bizarrely, ALTER_TRANS appears to be case-sensitive.

    If I do

    <<<<<<<< .../stratagems-inline/test.d
    ALTER_TRANS bdff1709
    BEGIN 2 END
    BEGIN 1 END
    BEGIN
    	"EPILOGUE" "EXTERN bdff1709 0"
    END
    
    ALTER_TRANS bdff1709
    BEGIN 4 END
    BEGIN 0 END
    BEGIN
    	"EPILOGUE" "EXTERN bdff1709 7"
    END
    
    >>>>>>>>
    
    COMPILE ".../stratagems-inline/test.d"

    then I can reproduce Jastey's bug.

    If I do

    <<<<<<<< .../stratagems-inline/test.d
    ALTER_TRANS bdff1709
    BEGIN 2 END
    BEGIN 1 END
    BEGIN
    	"EPILOGUE" "EXTERN BDFF1709 0"
    END
    
    ALTER_TRANS bdff1709
    BEGIN 4 END
    BEGIN 0 END
    BEGIN
    	"EPILOGUE" "EXTERN BDFF1709 7"
    END
    
    >>>>>>>>
    
    COMPILE ".../stratagems-inline/test.d"

    it works fine.

    Probably worth reporting to Wisp - it's undocumented as far as I can see.

  10. If you do

    PHP_EACH array AS k=>v

    for a multidimensional or variable-dimensional array, it cycles through all elements of the array. v is set to the value; k_0 is set to the first key, k_1 to the second key (if any), k_2 to the third (if any), and so on. k is a synonym for k_0. (This is documented in the WEIDU readme but quite obscurely - I only understood it myself recently and I've hardly ever seen it used in a mod in the wild, though I've started using it myself quite a lot.)

    That's often an inefficient way to cycle through a 2da that you've read in, of course; that's indeed why I read in rows and columns. If you want, say, to go through all entries in a particular column, it's easier to do something like

    PHP_EACH clastext_rows AS row=>discard BEGIN
    	desc_here=$clastext_array("%row%" "descstr")
    	[do something with row and desc_here]
    END

     

  11. 14 hours ago, Skye said:

    Since there's actually no such thing as a 2-dimensional array in WeiDU (to the best of my knowledge), it can't be a function.

    WEIDU is fine with multidimensional (or variable-dimensional) arrays. You can have a function that reads a 2da and spits out a 2D WEIDU array labelled by rows and columns. This is my (probably overcomplicated) version:

    Basic use:

    COPY_EXISTING - "clastext.2da" nowhere
    	LPF 2da_read RET_ARRAY clastext_array=array clastext_rows=rows clastext_cols=columns END

    Code:

    //////////////////////////////////////////////////////////////////////////////////////
    /*
    document{2da_read}
    {
    Read a 2da file (or, in patch context, the current 2da file) into a 2d array. Also return an array of uppercased row headers and
    column headers, in the format row_label=>row_number. ('case' controls the case of the
    row and column headers; it's uppercase by default on genuine 2das, mixed by default otherwise)
    
    In action context, if the file doesn't exist return 0; otherwise, return 1. Also
    whine if it doesn't exist, unless silent=1.
    
    If the file is a 2da, and 'reflect' is set, reverse rows and columns. If it's a 2da, and 'allow_incomplete_lines'
    is set, don't require that all lines are complete. If it's a 2da, and "rowname_column" is set, use that column
    (if it's present) for the row names instead of the usual entries.
    
    If 'rowmap' and/or 'colmap' are set, they get applied to the row and column entries before the array is constructed.
    
    If 'allow_incomplete_lines' is set, we can handle incomplete lines (filling with the default).
    }
    */
    //////////////////////////////////////////////////////////////////////////////////////
    
    DEFINE_PATCH_FUNCTION "2da_read"
    	INT_VAR silent=0//boolean
    			reflect=0//boolean
    			allow_incomplete_lines=0//boolean
    	STR_VAR type=""//[2da|ids|table_header|table_no_header]
    			rowmap=""//function
    			colmap=""//function
    			rowname_column=""
    			case=""//[upper|lower|mixed]
    	RET_ARRAY columns rows array
    BEGIN
    	// initialize
    	CLEAR_ARRAY columns
    	CLEAR_ARRAY rows
    	CLEAR_ARRAY array
    	// try to infer type
    	PATCH_IF "%type%" STR_EQ "" BEGIN
    		PATCH_IF "%rowmap%" STR_CMP "" || "%colmap%" STR_CMP "" BEGIN
    			SPRINT type "2da"
    		END ELSE BEGIN
    			READ_ASCII 0x0 sig (3)
    			PATCH_MATCH "%sig%" WITH 
    			"2da" "ids" BEGIN
    				SPRINT type "%sig%"
    			END
    			DEFAULT
    				PATCH_MATCH "%SOURCE_EXT%" WITH 
    				"2da" "ids" BEGIN
    					SPRINT type "%SOURCE_EXT%"
    				END
    				DEFAULT
    					SPRINT type "table_header"
    				END
    			END
    		END
    	END
    	// get column width & length
    	COUNT_2DA_COLS colcount
    	COUNT_2DA_ROWS colcount rowcount
    	PATCH_MATCH "%type%" WITH
    	"2da" BEGIN
    		// get the default
    		READ_2DA_ENTRY 1 0 1 default
    		// check if it's empty
    		COUNT_2DA_ROWS 1 empty_check
    		is_empty=(empty_check=3)
    		// get the col headers (& also the lookup column if appropriate)
    		lookup_col_num=0
    		COUNT_2DA_COLS colcount
    		COUNT_2DA_ROWS colcount rowcount
    		PATCH_MATCH "%colcount%" WITH
    		2 BEGIN
    			col_row=2
    			main_row=1
    		END
    		3 BEGIN
    			col_row=1
    			main_row=0
    		END
    		DEFAULT
    			col_row=0
    			main_row=0
    		END
    		READ_2DA_ENTRIES_NOW 2da_coldata (colcount - 1)
    		FOR (col=1;col<colcount;++col) BEGIN
    			READ_2DA_ENTRY_FORMER 2da_coldata col_row (col - 1) value
    			PATCH_MATCH "%value%" WITH 
    			"%rowname_column%" BEGIN
    				lookup_col_num=col
    			END
    			DEFAULT
    			END
    			PATCH_MATCH "%case%" WITH
    			lower BEGIN
    				TO_LOWER value 
    			END
    			upper BEGIN
    				TO_UPPER value
    			END	
    			mixed BEGIN
    			END
    			DEFAULT
    				TO_UPPER value
    			END
    			PATCH_IF !reflect BEGIN
    				SET $columns("%value%")=col
    			END ELSE BEGIN
    				SET $rows("%value%")=col			
    			END
    		END
    		PATCH_IF is_empty BEGIN
    			LPF array_map STR_VAR array=columns keymap="%colmap%" RET_ARRAY columns=array END
    			SPRINT $rows("null") discard
    			SPRINT $array("null") discard
    		END ELSE BEGIN
    			// get the rows
    			READ_2DA_ENTRIES_NOW 2da_data colcount
    			FOR (rownum=main_row;rownum<2da_data;++rownum) BEGIN
    				READ_2DA_ENTRY_FORMER 2da_data rownum lookup_col_num value
    				PATCH_MATCH "%case%" WITH
    				lower BEGIN
    					TO_LOWER value 
    				END
    				upper BEGIN
    					TO_UPPER value
    				END	
    				mixed BEGIN
    				END
    				DEFAULT
    					TO_UPPER value
    				END		
    				PATCH_IF !reflect BEGIN	
    					SET $rows("%value%")=rownum
    				END ELSE BEGIN
    					SET $columns("%value%")=rownum
    				END
    			END
    			// map rows and columns if needed
    			LPF array_map STR_VAR array=columns keymap="%colmap%" RET_ARRAY columns=array END
    			LPF array_map STR_VAR array=rows keymap="%rowmap%" RET_ARRAY rows=array END
    			
    			PATCH_IF allow_incomplete_lines BEGIN
    				// get the data into a working array
    				READ_ASCII 0x0 data (BUFFER_LENGTH)		
    				LPF data_lines STR_VAR data RET_ARRAY lines END
    				PHP_EACH lines AS ind=>line BEGIN
    					PATCH_IF ind>=3 BEGIN
    						LPF array_values_from_string INT_VAR quick=1 STR_VAR string="%line%" RET_ARRAY temparray=array END
    						LPF array_length STR_VAR array=temparray RET length END
    						SPRINT row $temparray(0)
    						row_int=ind - 3 +main_row
    						PATCH_IF !reflect BEGIN
    							SPRINT columns_or_rows columns
    						END ELSE BEGIN
    							SPRINT columns_or_rows rows
    						END
    						PHP_EACH "%columns_or_rows%" AS col=>int BEGIN
    							PATCH_IF int<=length BEGIN
    								SPRINT $working_array("%row_int%" "%int%") $temparray("%int%")
    							END ELSE BEGIN
    								SPRINT $working_array("%row_int%" "%int%") "%default%"
    							END
    						END
    					END			
    				END
    				// get it into the final array
    				PHP_EACH rows AS row=>row_int BEGIN
    					PHP_EACH columns AS col=>col_int BEGIN
    						PATCH_IF !reflect BEGIN
    							SPRINT $array("%row%" "%col%") $working_array("%row_int%" "%col_int%")
    						END ELSE BEGIN
    							SPRINT $array("%row%" "%col%") $working_array("%col_int%" "%row_int%")				
    						END
    					END		
    				END
    			END ELSE BEGIN
    				PHP_EACH rows AS row=>row_int BEGIN
    					PHP_EACH columns AS col=>col_int BEGIN		
    						PATCH_IF !reflect BEGIN
    							READ_2DA_ENTRY_FORMER 2da_data row_int col_int entry
    						END ELSE BEGIN
    							READ_2DA_ENTRY_FORMER 2da_data col_int row_int entry
    						END
    						SPRINT $array("%row%" "%col%") "%entry%"
    					END
    				END	
    			END
    		END
    	END
    	"ids" BEGIN
    		READ_2DA_ENTRIES_NOW 2da_data 2
    		// set cols
    			SET $columns("int")=0
    			SET $columns("sym")=1
    		// get main data
    		count=0
    		FOR (row=0;row<2da_data;++row) BEGIN
    			READ_2DA_ENTRY_FORMER 2da_data row 0 int
    			READ_2DA_ENTRY_FORMER 2da_data row 1 sym
    			PATCH_IF IS_AN_INT int BEGIN
    				SET $array("%count%" "int")=int
    				SPRINT $array("%count%" "sym") "%sym%"
    				++count
    			END
    		END
    		// set rows
    		FOR (row=0;row<count;++row) BEGIN
    			SET $rows("%row%")=row
    		END
    
    	END	
    	"table_header" BEGIN
    		COUNT_2DA_COLS colcount
    		READ_2DA_ENTRIES_NOW 2da_data colcount
    		// get columns
    		FOR (col=0;col<colcount;++col) BEGIN
    			READ_2DA_ENTRY_FORMER 2da_data 0 col value
    			PATCH_MATCH "%case%" WITH
    			lower BEGIN
    				TO_LOWER value 
    			END
    			upper BEGIN
    				TO_UPPER value
    			END	
    			DEFAULT
    			END
    			SET $columns("%value%")=col
    		END
    		// get data; set rows
    		FOR (ind=1;ind<2da_data;++ind) BEGIN
    			row=ind - 1
    			SET $rows("%row%")=row
    			PHP_EACH columns AS col=>colnum BEGIN
    				READ_2DA_ENTRY_FORMER 2da_data ind colnum value
    				SPRINT $array("%row%" "%col%") "%value%"
    			END
    		END
    	END
    	"table_noheader" BEGIN
    		COUNT_2DA_COLS colcount
    		READ_2DA_ENTRIES_NOW 2da_data colcount
    		// set columns
    		FOR (col=0;col<colcount;++col) BEGIN
    			SET $columns("%col%")=col
    		END
    		// get data; set rows
    		FOR (ind=1;ind<2da_data;++ind) BEGIN
    			row=ind - 1
    			SET $rows("%row%")=row
    			PHP_EACH columns AS col=>colnum BEGIN
    				READ_2DA_ENTRY_FORMER 2da_data ind colnum value
    				SPRINT $array("%row%" "%col%") "%value%"
    			END
    		END
    	
    	END
    	DEFAULT
    		PATCH_FAIL "2da_read: unidentified read type %type%"
    	END
    
    END

     

  12. On 10/15/2021 at 6:28 PM, subtledoctor said:

    the current updated version of the mod leaves the Silver Sword as a non-artifact weapon that hits as +3. But again, while this may sound "weaker," I challenge anyone to write a list of enemies who can't be hit by such weapons. Off the top of my head, I can only think of two. Maybe three.

    Four in the unmodded game, I think: Fallen Solars, the Ravager, the Aurumach Rilmani, and the Lesser Demon Lord.

    (And any wizard who casts Improved Mantle, of course!)

  13. 1 minute ago, subtledoctor said:

    unless you mean in OBG1? (I don’t even really consider that to be a game under discussion - the engine is so primitive, and none of these mods are built to work with it, apart from some stuff in CDTweaks and maybe SCS?)

    No, SCS has never worked for oBG1. My embryonic, pre-public, pre-tutu version was written for BG1, but I rapidly decided it was too much of a pain. (Pausing the game can interrupt enemy spellcasting.)

×
×
  • Create New...