Jump to content

Reverse Engineering FX


Lindor

Recommended Posts

Wow you already spotted a mistake I made.:o

4 hours ago, Flix said:

FX_ALIENSMOKE - green smoke

I assumed that would be from the alien questline in the high elf region, so I sorted it onto environmental (which is probably the most error-prone of the lists). Man, you're awesome:bow:

4 hours ago, Flix said:

FX_SKELETON_FROSTSHIELD - amazing snowy shield aura/buff - replacement for Holy Frost or Hurricane (if resizing possible)?

There's also an FX_SKELETON_FIRESHIELD and FX_SKELETON_POISONSHIELD.

Also FX_ENEMY_SPIKESHIELD_C and FX_FIRESHIELD and FX_FIRESHIELD_C could be something similar

Edited by Lindor
  • Like! 1
Link to comment

Unfortunately the copy-pasting didn't transfer the formatting. The coloring I had was pretty important, since that was literally the point of the lua-scripting: to get a the connection between particle scripts and used FX so that we can deep-dive on the unused ones. I re-did that manually now for the more interesting FX.

One additional note: all the Environmental FX in the uninteresting particle script section don't have a corresponding FX in spells.txt, which makes these error-prone. I sorted these only judging by their name, but it could be that there are still some unused spell FX hiding in there.

Edited by Lindor
Link to comment

I think you misunderstood me a little bit here. I didn't want to post lua code. The lua scripting gave me multiple text files which I converted into a single .ods file and formatted it in openoffice calc. Then I simply copy-pasted the colums of my .ods file and that didn't transfer the formatting, which is why I partially re-did it manually now, aka coloring all particle script names in the two interesting lists with a corresponding FX in spells.txt yellow. But thanks to the advice, I appreciate it. Or did I misunderstand you and you want me to post my lua code? I can do that if you want, it's nothing complicated.

Edited by Lindor
Link to comment

Thanks Flix, now the zooming worked amazing. Tonight I went through some of the particle scripts without a corresponding entry in the merged FX file and took some screenshots. This is where I had luck:

FX_COBOLD_FUMBLE and FX_COBOLD_FUMBLE_C --like you described, a yellow-red bomb-like with a very annoying triple soundeffect
FX_ENEMY_HEAL and FX_ENEMY_HEAL_C --amazing blue casting particles
FX_SKELETON_HEAL --buff where actual conjured up shields sourround the caster (and they bounce a little)
FX_SKELETONHEAL --green poisonous cloud aura, if you look closely it also has a blue aura at the ground below the cloud
FX_SKELETON_POISONSHIELD -- buff, green poisonous doughnut surrounding the caster
FX_SKELETON_FIRESHIELD -- buff, enflames the ground in a sun pattern where the caster walks, also has green aura at the ground
FX_SKELETON_FROSTSHIELD -- buff, as you described an amazing doughnut-like snowstorm

Fore these particle scripts I couldn't find a possible FX entry so far:

fx_ghostreanim_impact.particle
fx_ghostreanim_impact2.particle
fx_ghostreanim_trail.particle

fx_enemy_heal2_c.particle --these ones are maybe just additional scripts for FX_ENEMY_HEAL and FX_ENEMY_HEAL_C
fx_enemy_heal2.particle

fx_ghostheart.particle --this makes me sad, I had big hopes here. Maybe try it again with different spellclasses?

fx_enemy_spawn_demon.particle

 

I'll edit the post and upload the images next. To be honest editing the screenshots took me like ten times the time I needed to test these particle scripts for possible FX lol.

Link to comment

Said and done. I also cleaned up the original post, it was a bit messy. I split it into three parts, transfered all successfully reverse engineered spelld FX into a single spoiler section to increase the overview and split the yellow particle scripts from the blue and red ones so the dividing into most interesting, midly interesting and uninteresting for reviving dead spell FX makes more sense now.

  • Like! 1
Link to comment

That is a very good advice, I'm going to do that next. Also I spotted a mistake that I made that is that the AS module list is actually a second CORE list. I'm going to correct things tonight and sort out all of the revived FX from S2EE and D2F. Additionally I will sort out all the FX for the unused leftover spells (the ones with xxx and zzz as a prefix). Lastly I want to post screenshots to all of these sorted out FX, but that might take me a little longer.

Link to comment

Okay after more than five hours of error backtracing, I think I've got it. I can only give the tip to never, ever use the table.sort() or table.remove() functions, better write your own

 Eight text files: the four FX text files for CM, CORE, AS and D2F spells.txt and in that hierarchy three additional text files for all FX not used in any of the previous spells.txt and lastly the merged text file. If everything works correctly, then there are only two FX in D2F that are not used in any of the previous spells.txt. Sounds wierd at first, but not when you think about the leftover xxx and zzz spells which are still contained in CM.

This is my code for anyone interested:

I ran this code four times, once per spells.txt:

Spoiler


local mgr = {}
local Spells = {}

function mgr.defineSpell(a, b)
    Spells[a] = b
end

local entries = {
"fxTypeCast",
"fxTypeSpell",
"fxTypeCastSpecial",
}

--here I copy-pasted all the spell defines from the corresponding spells.txt. I'll only copy the first one as an example here.
mgr.defineSpell( "enemy_gen_chainlightning", {
	eiStateName = "cSpellCast",
	fxTypeCast = "",
	fxTypeSpell = "FX_HE_ENERGIEBLITZ",
	fxTypeCastSpecial = "",
	duration = 15.000000,
	animType = "ANIM_TYPE_MAGICA",
	animTypeApproach = "ANIM_TYPE_INVALID",
	animTypeRide = "ANIM_TYPE_INVALID",
	animTypeSpecial = "ANIM_TYPE_INVALID",
	causesSpellDamage = 1,
	tokens = {
		entry0 = {"et_chance_chain_nr", 850, 2, 0, 4 },
		entry1 = {"et_spelldamage_ice", 560, 280, 0, 133 },
		entry2 = {"et_spelldamage_physical", 420, 210, 0, 133 },
		entry3 = {"et_closedown_buff", 1000, 5, 0, 5 },
	},
	fightDistance = 525.000000,
	aspect = "EA_ENEMY_ANY",
	cooldown = 2.000000,
	soundProfile = 0,
	cost_level = 200,
	cost_base = 300,
	focus_skill_name = "skill__enemy_focus",
	lore_skill_name = "skill__enemy_lore",
	spellClass = "cSpellHeEnergieblitz",
	spellcontroltype = "eCAtype_a_effect_attack",
	sorting_rank = 0,
})
--that's it

function sort(arr)
    local left = {}
    local right = {}
    local pivot = arr[math.floor(#arr / 2)]
    local pivots = {}
    for k, v in pairs(arr) do
        if v < pivot then
            left[#left + 1] = v
        end
        if v > pivot then
            right[#right + 1] = v
        end
        if v == pivot then
            pivots[#pivots + 1] = v
        end
    end
    for k, v in pairs(pivots) do
        local c = 0
        if #right <= #left then
            right[#right + 1] = v
            c = 1
        end
        if #right > #left then
            if c == 0 then
                left[#left + 1] = v
            end
        end
    end
    return {left, right}
end

function quicksort(x)
    local r = #x
    if r == 1 then
        return x
    end
    local y = sort(x)
    local I = 1
    while I < r do
        if #y[I] > 1 then
            local z = sort(y[I])
            for k = 0, #y - I + 1 do
                y[#y - k + 1] = y[#y - k]
            end
            z = sort(y[I])
            y[I+1] = z[2]
            y[I] = z[1]
        end
        if #y[I] == 1 then
            I = I + 1
        end
    end
    for b = 1, #y do
        y[b] = y[b][1]
    end
    return y
end

function copy(a)
    local b = {}
    for I = 1, #a do
        if a[I] ~= nil then
            b[#b + 1] = a[I]
        end
    end
    b = quicksort(b)
    return b
end

function nilkill(a)
    local b = copy(a)
    local c = {}
    for I = 1, #b do
        if b[I] ~= nil then
            do c[#c + 1] = b[I] end
        end
    end
    c = quicksort(c)
    return c
end

function doublekill(a)
    local b = copy(a)
    local c = {}
    local d = {}
    for k, v in pairs(b) do
        c[v] = k
    end
    for k, v in pairs(c) do
        d[#d + 1] = k
    end
    d = quicksort(d)
    return d
end

local file = io.open("CM.txt", "w")

local u = {}



for k, v in pairs(Spells) do
    print(k)
    if v ~= nil then
        for k = 1, #entries do
            local b = entries[k]
            if v[b] == nil or v[b] == "" or v[b] == {} or v[b] == {{}} then
                v[b] = nil
            end
            u[#u + 1] = v[b]
        end
    end
end

u = doublekill(u)
u = quicksort(u)

for n = 1, #u do
    local o = u[n]
    file:write(o, "\n")
end
file:close()

 

This code only one time:

Spoiler


fh,err = io.open("CM.txt")
if err then
	print(":eek: happens")
    io.read()
    return
end
local CM = {}
while true do
    line = fh:read()
    if line == nil then
        break
    end
    CM[#CM + 1] = tostring(line)
end
fh:close()

fh,err = io.open("CORE.txt")
if err then
	print(":eek: happen")
    io.read()
    return
end
local CORE = {}
while true do
    line = fh:read()
    if line == nil then
        break
    end
    CORE[#CORE + 1] = tostring(line)
end
fh:close()

fh,err = io.open("AS.txt")
if err then
	print(":eek: happen")
    io.read()
    return
end
local AS = {}
while true do
    line = fh:read()
    if line == nil then
        break
    end
    AS[#AS + 1] = tostring(line)
end
fh:close()

fh,err = io.open("D2F.txt")
if err then
	print(":eek: happen")
    io.read()
    return
end
local DZF = {}
while true do
    line = fh:read()
    if line == nil then
        break
    end
    DZF[#DZF + 1] = tostring(line)
end
fh:close()

--this is a leftover from the error backtracing
local testvar = 0

function sort(arr)
    local left = {}
    local right = {}
    local pivot = arr[math.floor(#arr / 2)]
    local pivots = {}
    for k, v in pairs(arr) do
        if v < pivot then
            left[#left + 1] = v
        end
        if v > pivot then
            right[#right + 1] = v
        end
        if v == pivot then
            pivots[#pivots + 1] = v
        end
    end
    for k, v in pairs(pivots) do
        local c = 0
        if #right <= #left then
            right[#right + 1] = v
            c = 1
        end
        if #right > #left then
            if c == 0 then
                left[#left + 1] = v
            end
        end
    end
    return {left, right}
end

function quicksort(x)
    local r = #x
    if r == 1 then
        return x
    end
    local y = sort(x)
    local I = 1
    while I < r do
        if #y[I] > 1 then
            local z = sort(y[I])
            for k = 0, #y - I + 1 do
                y[#y - k + 1] = y[#y - k]
            end
            z = sort(y[I])
            y[I+1] = z[2]
            y[I] = z[1]
        end
        if #y[I] == 1 then
            I = I + 1
        end
    end
    for b = 1, #y do
        y[b] = y[b][1]
    end
    return y
end

function copy(a)
    local b = {}
    for I = 1, #a do
        if a[I] ~= nil then
            b[#b + 1] = a[I]
        end
    end
    b = quicksort(b)
    return b
end

function nilkill(a)
    local b = copy(a)
    local c = {}
    for I = 1, #b do
        if b[I] ~= nil then
            do c[#c + 1] = b[I] end
        end
    end
    c = quicksort(c)
    return c
end

function doublekill(a)
    local b = copy(a)
    local c = {}
    local d = {}
    for k, v in pairs(b) do
        c[v] = k
    end
    for k, v in pairs(c) do
        d[#d + 1] = k
    end
    d = quicksort(d)
    return d
end

function subtr(a, b)
    local c = copy(a)
    local d = copy(b)
    local e = {}
    for I = 1, #c do
        local f = 0
        for j = 1, #d do
            if c[I] == d[j] then
                f = 1
                break
            end
        end
        if f == 0 then
            e[#e + 1] = c[I]
        end
    end
    local e = quicksort(e)
    return e
end

function mergenext(a, b)
    local c = copy(a)
    local d = copy(b)
    for I = 1, #c do
        d[#d + 1] = c[I]
    end
    do d = doublekill(d) end
    return d
end

function writenext(a, b, c)
    local file = io.open(a, "w")
    local list = subtr(b, c)
    for I = 1, #list do
        local line = list[I]
        file:write(line, "\n")
    end
    file:close()
end

local merged = {}
local helper = {}
local helpy = {}

--don't judge too harshly here, I did everything over-correctly here for the error.backtracing         
do helper = mergenext(CM, helpy) end
do helpy = {} end
do merged = copy(helper) end
do helpy = copy(helper) end
do helper = {} end
do writenext("CORE subtracted.txt", CORE, merged) end
do merged = {} end

do helper = mergenext(CORE, helpy) end
do helpy = {} end
do merged = copy(helper) end
do helpy = copy(helper) end
do helper = {} end
do writenext("AS subtracted.txt", AS, merged) end
do merged = {} end

do testvar = 1 end
do helper = mergenext(AS, helpy) end
do helpy = {} end
do merged = copy(helper) end
do helpy = copy(helper) end
do helper = {} end
do writenext("D2F subtracted.txt", DZF, merged) end
do merged = {} end

do helper = mergenext(DZF, helpy) end
do helpy = {} end
do merged = copy(helper) end
do helper = {} end

local x = io.open("merged.txt", "w")
do merged = doublekill(merged) end
do merged = quicksort(merged) end
for I = 1, #merged do
    local a = merged[I]
    x:write(a, "\n")
end
x:close()

 

I like to do everything as simple as possible, no functions calling itself, no metatables etc. I'm completely noob and I always have a hard time on the backtracing on that kind of stuff :)

 

I'll update the first post next so you can all see my results.

 

EDIT: I have a feeling that the --ignore does not really workon posting lua code :D 

Edited by Lindor
Link to comment

I revived some more:

  • FX_FROSTSCHOCK_C --as you described, bright blue icy casting FX
  • FX_ENEMY_TENERGY_EXPLOSION --amazing magic-blue nova
  • FX_FROSTKREIS_C --magic Version of FX_FROSTSCHOCK_C (which I think is wierd)
  • FX_GHOSTSCREAM_C --like a smaller Version of FX_FROSTSCHOCK_C
  • FX_SPPL_RISE --awesome sand-dusty cylinder with yellow sparkles
  • FX_KEHLENREISSER_C --pretty long casting animation without any particles, shes kneeing and waving her hand
  • FX_ENEMY_T_CONE_STONE_ATTACH_C --audio only, like a deeper and less loud version of a critical hit or a rock hitting something
  • FX_GHOSTTELEKINESE_C --audio only, like a normal hit, don't really know how to describe but it's a little bit underwhelming
On 12/28/2020 at 11:30 PM, Flix said:

FX_ALIENSMOKE - green smoke
FX_FROSTSCHOCK - an icy explosion on the target

Which spellclasses did you use? Because I could not replicate that. Would also be interesting for FX_FROSTKREIS and FX_GHOSTSCREAM which I couldn't find a working spellclass for as well.

I'm not gonna upload the screenshots today btw in case you're wondering.

  • Like! 1
Link to comment

By the way, those bouncing shields are the generic "defensive"  buff FX that are hard coded to appear whenever a spell grants certain properties and there are no other effects.  You can see them all the time when the player or a minion receives a boost to defense or damage mitigation (Augmenting Guidon, Ruinous Onslaught, Shadow Step w/ Sanctuary mod).  It can also be forced but I don't remember the exact name.

There is an equivalent offensive FX with rotating crossed swords, that can also be forced with FX_GEN_MEHR_AW.

3 hours ago, Lindor said:

Which spellclasses did you use? Because I could not replicate that.

cSpellDmSchutzrunen and cSpellHeFrostschlag IIRC.

Link to comment

I know why FX_FROSTSCHOCK and FX_FROSTKREIS look like the same FX. That particle effect is not part of the FX, but built into the cSpellHeFrostschlag spellclass. I tested many more FX today and everytime I use this spellclass, the particle effect appears! [insert facepalm smiley here] What a complicated riddle FX are!

This could also probably be the cause for that mysterious game crash on FX_GHOSTSCREAM usage. We'll need to take a second look on all these FX.

 

EDIT: after a second look, I think that all spellclasses that come with spellcontroltype = "eCAtype_a_effect_attack_ray" may have a built-in particle effect

Edited by Lindor
Link to comment

The spellclass was not the cause for the game crash. I tried like a dozen different spellclasses, crash every time except on 
    spellClass = "cSpellSpawnEnemy",
    spellcontroltype = "eCAtype_a_effect_location_start",
or other enemy spawning spellclasses, where it didn't do anything (I mean that quite literally, the spell regeneration time didn't even start)

I got none of the three FX to work. Additionally I tried the last remaining particle scripts in the NPC spell section earlier, could not get anything to work. Guess I'm gonna shift the attention to the general hero effects section next. Makes me a little bit sad because there's still names left that looked like they'd have some potential.

Edited by Lindor
Link to comment

HUUGE step forward. I got like dozens of new spells revived from the general hero effects section! Screenshots are already added. Also I re-did most of the old screenshots because I was getting annoyed by the white squares, so I improved my GIMP skill and managed to align the screenshots without stretching them.:yay:

 

Every time I want to edit the first post it takes me longer and longer. I'm currently thinking about turning this topic into a discussion only topic and opening a second topic for the results only, where I then would be able to split the post into multiple posts without discussions appearing inbetween. What do you think about that?:)

 

EDIT:

Also I figured this one out:

On 12/31/2020 at 3:21 PM, Flix said:

There is an equivalent offensive FX with rotating crossed swords, that can also be forced with FX_GEN_MEHR_AW.

It's FX_GEN_MEHR_VW.

Another thought: FX_GEN_MEHR_AW, FX_GEN_MEHR_VW, FX_GEN_WENIGER_HEILUNG, how much more of these exist? ("mehr" means "more" and "weniger" means "less" or "fewer" in german). I have a feeling that there are more of these kinds of hardcoded FX without a particle script.

Edited by Lindor
Link to comment
3 hours ago, Lindor said:

I got like dozens of new spells revived from the general hero effects section! Screenshots are already added.

Really awesome examples you've found!

3 hours ago, Lindor said:

Every time I want to edit the first post it takes me longer and longer.

It seems like there's almost too much going on there, all these nested spoilers are tedious to browse.  I'm sure it's not much fun to try to edit them either.  You might simplify by just keeping a complete FX list, perhaps divided into used/unused (or tested/untested), and then another section for results.

3 hours ago, Lindor said:

how much more of these exist?

As far as automatic FX based on spell tokens, there's FX_GEN_SPEEDUP, that's like rings of orange sparks that circle the player, triggers whenever movement or attack speed buffs apply.

There's FX_ENEMY_SPIKESHIELD_C, triggers for reflective buffs.  And FX_GEN_CASTBLOCK that weird thing that looks like a spinning top above the target's head, activated in CM 1.60, indicates some debuff or other, can't remember what.

Link to comment
On 1/2/2021 at 9:13 AM, Flix said:

Really awesome examples you've found!

Thanks!

On 1/2/2021 at 9:13 AM, Flix said:

It seems like there's almost too much going on there, all these nested spoilers are tedious to browse.  I'm sure it's not much fun to try to edit them either.  You might simplify by just keeping a complete FX list, perhaps divided into used/unused (or tested/untested), and then another section for results.

Hmm I never thought about that. I don't have a hard time finding what I'm looking for honestly, probably because everything comes out of my mind. The problem I'm facing when editing it is that it gets super laggy. I think about simplifying the FX, but I don't want to simplify the particle script sorting since that's literally the point of all the coding work I've done: So that me but also other people who want to search for and revive unused FX have an easier time on where to look and where to start.

On 1/2/2021 at 9:13 AM, Flix said:

As far as automatic FX based on spell tokens, there's FX_GEN_SPEEDUP, that's like rings of orange sparks that circle the player, triggers whenever movement or attack speed buffs apply.

There's FX_ENEMY_SPIKESHIELD_C, triggers for reflective buffs.  And FX_GEN_CASTBLOCK that weird thing that looks like a spinning top above the target's head, activated in CM 1.60, indicates some debuff or other, can't remember what.

Very interesting examples you bring up here, but I had something different in mind: FX_GEN_MEHR_AW, FX_GEN_MEHR_VW and FX_GEN_WENIGER_HEILUNG are indicators to me that there probably was once planned a whole bigger system for automatically triggering VFX that later got cut, like that a FX_GEN_MEHR_ would be triggered everytime you'd get buffed and likewise a FX_GEN_WENIGER_ would be triggered everytime you'd get debuffed for something. "WENIGER" and "MEHR" are like Masaka and Korgano, they always go together. I was suggesting to try out the counterparts FX_GEN_WENIGER_AW, FX_GEN_WENIGER_VW and FX_GEN_MEHR_HEILUNG and likewise try it out with other things that can be buffed and debuffed, like ATTRIBUT(E)/STAT(S)/AP, LEBENSPUNKTE/LEBEN/HITPOINTS/HIT_POINTS/HP/LP, ATTACKSPEED/AS/ANGRIFFSGESCHWINDIGKEIT/AG, such things. It would be like a guessing game, but maybe something useful comes out.

 

EDIT: Btw I forgot to tell in the note that KEHLENREISSER would translate to THROATRIPPER or something like that, which is an indicator to why the spray vectors are into the wrong direction. The wrong direction of the ink spray is for a whole different reason, namely that like many bosses Octagolamus's body consists of multiple parts and the casting vector depends on the body part that is source of the spray. I'm not gonna edit that in though, I'll just gonna leave it here.

 

Edited by Lindor
Link to comment

There were only some wierd ones left in the interesting particle scripts section, I've finished these this morning and added all the creenshots so that now what's left are only the probably environmental particle scripts in the midly interesting particle scripts section, which is also the biggest of the lists.

Link to comment

That's it. We're done. It took me all day, but I went through all the environmental FX. The results are, uhm, different than what I expected. Also only one made my game crash, which is pretty unexpected. Screenshots are already added. FX I got to work:

  • FX_DESERTDUST
  • FX_FALLINGSAND1-3
  • FX_FOUNTAIN1-9
  • FX_FOUNTAIN2_BLOOD --I think these next three are what you've hoped for with FX_KEHLENREISSER
  • FX_FOUNTAIN7_BLOOD
  • FX_FOUNTAIN8_BLOOD
  • FX_GEYSER_WATER1-3
  • FX_LAVA1 --looks like a fiery force field shield!
  • FX_LAVAFALL1
  • FX_LAVAFALL2 --so amazing! With the correct animation, we could be on to something here!:o
  • FX_PORTAL
  • FX_PORTAL2
  • FX_PUSTEBLUME
  • FX_QFX_CAST --I've accidentally sorted them wrongly. They're definitely worth a look! I sense RBoL:)
  • FX_QFX_DRECKIGESWASSER
  • FX_QFX_FLIEGENSCHWAM --VERY hard to see
  • FX_REAGENZGLAS --green casting FX, a little bit small to see
  • FX_SCULPTING --VERY hard to see
  • FX_SMOKEBIGBLACK, -MEDIUMBLACK, -SMALLBLACK, -SMALLBLACK2
  • FX_SMOKEBIGGREY, -MEDIUMGREY, -SMALLGREY, -SMALLGREY2
  • FX_SMOKEBIGGREY, -SMALLGREY, -SMALLGREY2
  • FX_SOLARSYSTEM --fiery FX that shine through objects!
  • FX_TLECK --looks like a t-energy jellyfish:lol:
  • FX_WATERBUBBLES
  • FX_WATERDASH
  • FX_WATERFALL
  • FX_WATERFALL2
  • FX_WINDBLOWNLEAFS

This means all what's left to do is adding the screenshots to all the leftover spell FX and already revived FX.

Edited by Lindor
Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...
Please Sign In or Sign Up