Many people seem to be puzzled by what I'm trying to do.
As one code listing says a thousand words,
below is the script I used to build concentrated vanilla mod for M2TW.

I never manually edited a single game file - it was just this script
containing a bunch of actions like:
  • In text file X
  • Match regular expression Y
  • And replace with Z


Regular expressions look scary at first, but you get used to them eventually.
That bold block at the end is what I changed to try new things.
Change something, rerun script, instant win. Most of my ideas turned out to be stupid,
but it took only minutes to try a new combination, so no big loss.

If you cannot program, you can do most of what a script like that is doing with
find&replace in any text editor in a few more minutes. Or someone could write
a tool for you - every programmer alive can write scripts that edit plain text files!

If instead of txt files and crude trigger system we could work on xml files and lua code
it would take only half as much code, be much more powerful, and much clearer.
ETW had every chance of being far more moddable than anything before!

This is my idea of good modding. What ETW turned into instead was just sad...
not just because the game was new - it's been years since the release, and
even the most trivial ETW modding feels like trying to perform laser eye surgery except
someone stole the lasers and all we have left is a kitchen knife and baseball bat
for anesthesia - the only kind of surgery still possible is helping a patient with big cyclops fetish.

So I had two choices: dropping Total War series, watching modding scene wither away,
and playing some first person shooters or something instead (incidentally MW2 is also
a lot less moddable than earlier games in the series - and the reason again is DLC
if you're wondering why - this is happening all over the place not just here);
or I could at least try doing something about it before everyone
forgets there was once such thing as game modding.

For all I know it is very likely to fail - publishers have been getting better and better
at keeping even pirates away (PlayStation 3...) - and compared with the kind of determination,
coding skills, and money available to both sides in that fight, locking out modders
can be just an afterthought - we're just one PowerPoint slide by some asshat from DLC sales
department away from it.

And realistically how much can I do? I still have a regular job to do, and other real life stuff,
and all this format hacking have been taking a lot of my gaming time, however fun it is most of the time ;-)
It's still worth a try. The concept of glorious defeat doesn't apply only in-game.

Spoiler Alert, click show to read: 


Spoiler Alert, click show to read: 


#!/usr/bin/ruby -w
require 'find_file'

class Mod
def initialize(&blk)
@files = {}
@save_as = {}
open_files!
instance_eval(&blk)
save!
end

def open(name, file_name, save_dir=nil)
save_as = File.join(*["output/concentrated_vanilla/data", save_dir, file_name].compact)
@files[name] = File.read(File.first_matching(file_name, 'data', 'data/packed', 'data/imperial-campaign'))
@save_as[name] = save_as
end

def modify(name)
@files[name] = yield(@files[name])
end

def save!
@files.each{|name, value|
File.open(@save_as[name], 'w') {|fh|
fh.print value
}
}
end

def open_files!
end
end

class M2TW_Mod < Mod
def open_files!
open 'character', 'descr_character.txt'
open 'engines', 'descr_engines.txt'
open 'settlement', 'descr_settlement_mechanics.xml'
open 'resources', 'descr_sm_resources.txt'
open 'walls', 'descr_walls.txt'
open 'buildings', 'export_descr_buildings.txt'
open 'units', 'export_descr_unit.txt'
open 'strat', 'descr_strat.txt', 'world/maps/campaign/imperial_campaign'
open 'mercenaries', 'descr_mercenaries.txt', 'world/maps/campaign/imperial_campaign'
open 'cultures', 'descr_cultures.txt'
open 'character_traits', 'export_descr_character_traits.txt'
open 'standing', 'descr_faction_standing.txt'
open 'regions', 'descr_regions.txt', 'world/maps/base'
open 'rebels', 'descr_rebel_factions.txt'
open 'sounds', 'descr_sounds_units.txt'
end

def nerf_rams!
#Rams should really suck - 50% attack, 50% health
modify('engines'){|file|
file.sub(/^(type\s+tortoise_ram.*?^attack_stat\s+)([^\r\n]+)/m){
$1 + "5, 2, no, 0, 0, melee, melee_simple, blunt"
}.sub(/^(type\s+tortoise_ram.*?^engine_health\s+)([^\r\n]+)/m){
$1 + "75"
}
}
end

def taxes_influence_on_growth!(influence=2.0)
# Taxes should influence settlement growth rates twice as much
modify('settlement'){|file|
file.gsub(/(<factor name="SPF_TAX_RATE_(?:BONUS|PENALTY)">\s+<pip_modifier value=")1.0(")/){
"#{$1}#{influence}#{$2}"
}
}
end

def resource_value!(value=1.5)
# Resources 50% more valuable
modify('resources'){|file|
file.gsub(/^(trade_value\s+)(\d+)/){ "#{$1}#{(value*$2.to_i).to_i}" }
}
end

def spy_cost!(mult)
# Spies twice the recruitment cost
modify('cultures'){|file|
file.gsub(/^(spy.*?\s+)(\d+)\b/) { "#{$1}#{($2.to_i * mult).to_i}"}
}
# Spies x2 more expensive
modify('character'){|file|
type = nil
file.gsub(/(?:^type\s*(\S+))|(?:^(wage_base\s*)(\d+))/) {
if $1
type = $1
$&
elsif type == 'spy'
"#{$2}#{($3.to_i*mult).to_i}"
else
$&
end
}
}
end

def campaign_movement_speed!(mult)
# Everyone move 75% faster
modify('character'){|file|
file.gsub(/^(starting_action_points\s+)(\d+)/){"#{$1}#{(1.75*$2.to_i).to_i}"}
}
end

def wall_strength!(wall_mult, tower_mult)
# Gates and walls 5x stronger
# towers 0% stronger
modify('walls'){|file|
t = nil
file.gsub(/(.*)/){|line|
if line =~ /^\s+(wall|gateway|tower|gate)/
t = $1
elsif line =~ /\A(\s*full_health\s*)(\d+)(.*)/
if t == 'tower'
line = "#{$1}#{($2.to_i*tower_mult).to_i}#{$3}"
else
line = "#{$1}#{($2.to_i*wall_mult).to_i}#{$3}"
end
end
line
}
}
end

def tower_fire_rate!(normal, flaming)
# 2x faster tower firing rate, flaming reload the same
modify('walls'){|file|
file.gsub(/^(\s+fire_rate\s+\S+\s+)(\d+)(\s+)(\d+)/) {
"#{$1}#{($2.to_i/normal).to_i}#{$3}#{(($4.to_i)/flaming).to_i}"
}
}
end

def wall_control_area!(mult)
# WAS: All towers are manned as long as any unit is inside walls
# NOW: Much higher tower control radius
modify('walls'){|file|
file.gsub(/(control_area_radius\s*)(\d+)/){ "#{$1}#{($2.to_i*mult).to_i}" }
}
end

def construction_time_one_turn!
# All buildings can be constructed in one turn
modify('buildings'){|file|
file.gsub(/^(\s+construction\s+)(\d+)/) { "#{$1}1" }
}
end

def building_cost!(cost_mult, mine_extra_mult)
# All buildings 50% more expensive, mines 100% more expensive extra (for total of 300%)
modify('buildings'){|file|
file.gsub(/^(building hinterland_(?:castle_)?mines.*?^\})/m) {
$1.gsub(/(cost\s+)(\d+)/) {"#{$1}#{(mine_extra_mult*$2.to_i).to_i}"}
}.gsub(/(cost\s+)(\d+)/) {"#{$1}#{(cost_mult*$2.to_i).to_i}"}
}
end

def mine_resource!(mult)
# Mines levels are 2x, for total of 3x more money
modify('buildings'){|file|
file.gsub(/(mine_resource\s+)(\d+)/m) {
"#{$1}#{($2.to_i*mult).to_i}"
}
}
end

def bodyguard_size!(mult)
# Bodyguards nerfed to half size, no more 1hp nerfing, as they were useless late in game
# TODO: Their price in custom battles should be reduced
modify('units'){|file|
file.gsub(/(.*\S.*\n)+/) {|para|
if para =~ /\Atype\s*.*Bodyguard/
para = para.sub(/^(soldier\s+\S+, )(\d+)/){ "#{$1}#{($2.to_i*mult).to_i}" }
end
para
}
}
end

def cavalry_cost!(normal, bodyguard)
# Heavy cavalry 50% more expensive.
# Take bodyguards to 25% down because they're half as numerous and
# they were just insanely expensive without this.
modify('units'){|file|
file.gsub(/(.*\S.*\n)+/) {|para|
if para =~ /^category\s*cavalry/
mult = normal
mult = bodyguard if para =~ /\Atype\s*.*Bodyguard/
para = para.sub(/(stat_cost\s*)([0-9, ]*)/) {
pre, data = $1, $2.split(/,\s*/).map{|x|x.to_i}
# 0=turns(1), 6=custom battle too-many-same-units penalty(4)
[1,2,3,4,5,7].each{|i| data[i] = (data[i]*mult).to_i}
"#{pre}#{data.join(', ')}"
}
end
para
}
}
end

def missile_infantry_ammo!(mult)
# More ammo for missile infantry
modify('units'){|file|
file.gsub(/^(category\s+infantry.*?stat_pri\s+\d+,\s*\d+,\s*\S+,\s*\d+,\s*)(\d+)/m){
"#{$1}#{($2.to_i*mult).to_i}"
}
}
end

def rebel_spawn_rates!(mult)
# Make rebels and pirates spawn 10x less often - doesn't really seem to work
modify('strat'){|file|
file.gsub(/^((?:pirate|brigand)_spawn_value\s+)(\d+)/){"#{$1}#{($2.to_i*mult).to_i}"}
}
end

def king_purse!(mult)
# Double king's purse to help small countries
modify('strat'){|file|
file.gsub(/^(denari_kings_purse\s+)(\d+)/){"#{$1}#{($2.to_i*mult).to_i}"}
}
end

def big_garrisons!
# More free garrisons
# Cities: 0 2 3 4 5 6 -> 0 4 6 8 10 12
# Castles: 0 0 0 0 0 -> 1 2 4 7 12
modify('buildings'){|file|
file.sub(/^(building core_building\s*\{)(.*?)(^\})/m){
prefix, description, suffix = $1,$2,$3
description = description.gsub(/(free_upkeep bonus )(\d+)/) {"#{$1}#{$2.to_i*2}"}
"#{prefix}#{description}#{suffix}"
}.sub(/^(building core_castle_building\s*\{)(.*?)(^\})/m){
prefix, description, suffix = $1,$2,$3
by_level = [0, 1, 2, 4, 7, 12]
description = description.gsub(/^(\s*)(law_bonus bonus )(\d+)(\s*?\n)/) { "#{$1}#{$2}#{$3}#{$4}#{$1}free_upkeep bonus #{by_level[$3.to_i]}#{$4}" }
"#{prefix}#{description}#{suffix}"
}
}
end

def basic_infantry_garrisonned_for_free!
# Basic non-merc infantry should be free upkeep - where basic means morale <= 5 and upkeep <= 155,
# what works quite well
modify('units'){|file|
file.gsub(/(.*\S.*\n)+/){|para|
if para =~ /\Atype/ and
para =~ /^category\s+infantry/ and
para !~ /attributes.*\b(free_upkeep_unit|mercenary_unit|general_unit)\b/ and
para !~ /^ownership\s+(slave|saxons)\s*$/

raise "Parse error: #{para}" unless para =~ /^stat_cost\s+\d+,\s+(\d+),\s+(\d+)/
r, u = $1.to_i, $2.to_i
raise "Parse error: #{para}" unless para =~ /stat_mental\s+(\d+)/
m = $1.to_i
para = para.sub(/(attributes[^\r\n]+)/){"#{$1}, free_upkeep_unit"} if m <= 5 && u <= 155

# c = [m <= 5 && u <= 155].select{|x|x}.size
# c += 10 if u == 0 # Irrelevant anyways
# para =~ /dictionary\s*(\S+)/
# puts "#{c} #{m} #{u} #{r} #{$1}"
end
para
}
}
end

def fix_standing!
modify('standing'){|file|
file.sub(/;Trigger 0102_city_razed_decrease_global.*?; make all other factions hate the rebels/m) {|part|
part.gsub(/^;*/, ';')
}
}
end

def fix_rubber_swords!
# Heavy cavalry 50% more expensive.
# Take bodyguards to 25% down because they're half as numerous and
# they were just insanely expensive without this.
modify('units'){|file|
file.gsub(/(.*\S.*\n)+/) {|para|
if para =~ /^formation.*phalanx/
para.sub!(/^(stat_sec\s+)(.*)/){"#{$1}0, 0, no, 0, 0, no, melee_simple, blunt, none, 25, 1"}
end
para
}
}
end

def change_regions!
modify('regions'){|file|
file.gsub(/^([A-Z].*\n(?:\t.*\n)+)/){|region|
yield(region)
}
}
end

def change_region_resources!
change_regions!{|region|
lines = region.split(/\n/)
lines[5] =~ /\A(\s*)(.*?)(\s*)\Z/
st, en = $1, $3
resources = $2.split(/,\s+/)
resources = [] if resources == ['none']
resources = yield(resources).uniq
resources = ['none'] if resources == []
lines[5] = st + resources.join(', ') + en
lines.join("\n") + "\n"
}
end

def no_rebels!
change_region_resources!{|res|
res + ['no_brigands', 'no_pirates']
}
end

def crusades_everywhere!
change_region_resources!{|res|
res += ['crusade', 'jihad', 'horde_target'] unless res.include? 'america'
res
}
end

def no_siege!
modify('buildings'){|file|
file.sub(/^building siege.*?^\}\r\n/m, '').sub(/^building castle_siege.*?^\}\r\n/m, '')
}
end

def all_mercenaries_available!
modify('mercenaries'){|file|
file.gsub(/^\s+unit.*$/){|line|
line.gsub(/ start_year \d+/, '').gsub(/ end_year \d+/, '').gsub(/ events \{(?: |gunpowder_discovered|mongols_invasion_warn)*\}/, '')
}
}
end

def all_buildings_available!
modify('buildings'){|file|
file.gsub(/ and event_counter (?:first_printing_press|gunpowder_discovered|world_is_round) 1/, '')
}
end
def reduce_captain_obvious!
modify('sounds'){|file|
file.gsub(/(unit_under_attack_delay )(\d+)/){ "#{$1}#{$2.to_i*100}"}
}
end
# def more_rebels!
# modify('rebels'){|file|
# file.gsub(/(chance\s+)3/){ "#{$1}50"}
# }
# end
end




M2TW_Mod.new do
spy_cost!(2.0)
campaign_movement_speed!(1.5)
wall_strength!(3.0, 1.0)
#tower_fire_rate!(1.0, 1.0)
wall_control_area!(8)
construction_time_one_turn!
#building_cost!(1.5, 2.0)
#mine_resource!(2)
bodyguard_size!(0.5)
cavalry_cost!(1.0, 0.5)
#missile_infantry_ammo!(1.5)
rebel_spawn_rates!(1000)
#king_purse!(2.0)
big_garrisons!
basic_infantry_garrisonned_for_free!
fix_standing!
fix_rubber_swords!
#no_rebels!
crusades_everywhere!
#more_rebels!
no_siege!
all_mercenaries_available!
all_buildings_available!
reduce_captain_obvious!

end