Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_brit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_brit.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_brit.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/brit_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_brit.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml (nonexistent) @@ -1,122 +0,0 @@ - - - - 80 - 160 - 60000 - - - 200 - 12 - 0.04 - 4 - 0 - 8 - - - - 50 - 1 - 10 - 0 - 1 - 1 - 1 - 1 - 5 - 2 - 0 - 2 - 4 - 1 - 1 - 1 - 30 - 20 - 1 - - - - 15 - - - 4 - - - 5 - - - - - phase_town - - - - - - Player - Player - true - - - - - 1.0 - 1.0 - 1.0 - 1.0 - - - 1.0 - 1.0 - 1.0 - 1.0 - - - unlock_shared_los - unlock_shared_dropsites - 1.0 - - - - 0.0 - 0.0 - 0.0 - 0.0 - - 1000 - - - - interface/alarm/alarm_defeated.xml - interface/alarm/alarm_defeated_ally.xml - interface/alarm/alarm_defeated_enemy.xml - interface/alarm/alarm_no_idle_unit.xml - - - - - - Cavalry - Champion - Domestic - FemaleCitizen - Hero - Infantry - Ship - Siege - Trader - Worker - - - Economic - CivCentre - Fortress - House - Military - Outpost - Wonder - - - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_iber.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_iber.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_iber.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/iber_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_iber.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaia.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaia.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaia.xml (nonexistent) @@ -1,22 +0,0 @@ - - - - - - 1.0 - - - 1.0 - 1.0 - 1.0 - 1.0 - - - 1.0 - 1.0 - 1.0 - 1.0 - - - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaia.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_rome.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_rome.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_rome.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/rome_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_rome.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_maur.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_maur.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_maur.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/maur_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_maur.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_mace.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_mace.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_mace.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/mace_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_mace.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaul.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaul.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaul.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/gaul_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_gaul.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_sele.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_sele.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_sele.xml (nonexistent) @@ -1,14 +0,0 @@ - - - - teambonuses/sele_player_teambonus - - - - - phase_town - Hero - - - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_sele.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_cart.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_cart.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_cart.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/cart_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_cart.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_spart.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_spart.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_spart.xml (nonexistent) @@ -1,4 +0,0 @@ - - - teambonuses/spart_player_teambonus - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_spart.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_pers.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_pers.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_pers.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/pers_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_pers.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_kush.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_kush.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_kush.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/kush_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_kush.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_ptol.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_ptol.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_ptol.xml (nonexistent) @@ -1,14 +0,0 @@ - - - - teambonuses/ptol_player_teambonus - - - - - phase_town - Hero - - - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_ptol.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_athen.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_athen.xml (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_athen.xml (nonexistent) @@ -1,6 +0,0 @@ - - - - teambonuses/athen_player_teambonus - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player_athen.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json (revision 26298) @@ -1,92 +1,89 @@ { "Code": "spart", "Culture": "hele", - "Name": "Spartans", - "Emblem": "session/portraits/emblems/emblem_spartans.png", - "History": "Sparta was a prominent city-state in ancient Greece, and its dominant military power on land from circa 650 B.C. Spartan culture was obsessed with military training and excellence, with rigorous training for boys beginning at age seven. Thanks to its military might, Sparta led a coalition of Greek forces during the Greco-Persian Wars, and won over Athens in the Peloponnesian Wars, though at great cost.", "Music": [ { "File": "Helen_Leaves_Sparta.ogg", "Type": "peace" }, { "File": "Peaks_of_Atlas.ogg", "Type": "peace" }, { "File": "Forging_a_City-State.ogg", "Type": "peace" }, { "File": "The_Hellespont.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Spartan Women", "History": "", "Description": "Female Citizens +40% health and +50% melee attack hack damage." } ], "WallSets": [ "structures/wallset_palisade", "structures/spart/wallset_stone" ], "StartEntities": [ { "Template": "structures/spart/civil_centre" }, { "Template": "units/spart/support_female_citizen", "Count": 4 }, { "Template": "units/spart/infantry_spearman_b", "Count": 2 }, { "Template": "units/spart/infantry_javelineer_b", "Count": 2 }, { "Template": "units/spart/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx" ], "AINames": [ "Leonidas", "Dienekes", "Brasidas", "Agis", "Archidamus", "Lysander", "Pausanias", "Agesilaus", "Echestratus", "Eurycrates", "Eucleidas", "Agesipolis" ], "SkirmishReplacements": { "skirmish/structures/default_house_10": "structures/{civ}/house", "skirmish/structures/default_wall_tower": "", "skirmish/structures/default_wall_gate": "", "skirmish/structures/default_wall_short": "", "skirmish/structures/default_wall_medium": "", "skirmish/structures/default_wall_long": "" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/athen.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/athen.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/athen.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/athen_player_teambonus + + + athen + Athenians + As the cradle of Western civilization and the birthplace of democracy, Athens was famed as a center for the arts, learning and philosophy. The Athenians were also powerful warriors, particularly at sea. At its peak, Athens dominated a large part of the Hellenic world for several decades. + session/portraits/emblems/emblem_athenians.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/athen.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json (revision 26298) @@ -1,82 +1,79 @@ { "Code": "gaul", "Culture": "celt", - "Name": "Gauls", - "Emblem": "session/portraits/emblems/emblem_celts.png", - "History": "The Gauls were the Celtic tribes of continental Europe. Dominated by a priestly class of Druids, they featured a sophisticated culture of advanced metalworking, agriculture, trade and even road engineering. With heavy infantry and cavalry, Gallic warriors valiantly resisted Caesar's campaign of conquest and Rome's authoritarian rule.", "Music": [ { "File": "Celtic_Pride.ogg", "Type": "peace" }, { "File": "Cisalpine_Gaul.ogg", "Type": "peace" }, { "File": "Harvest_Festival.ogg", "Type": "peace" }, { "File": "Water's_Edge.ogg", "Type": "peace" }, { "File": "Sunrise.ogg", "Type": "peace" } ], "CivBonuses": [], "WallSets": [ "structures/wallset_palisade", "structures/gaul/wallset_stone" ], "StartEntities": [ { "Template": "structures/gaul/civil_centre" }, { "Template": "units/gaul/support_female_citizen", "Count": 4 }, { "Template": "units/gaul/infantry_spearman_b", "Count": 2 }, { "Template": "units/gaul/infantry_javelineer_b", "Count": 2 }, { "Template": "units/gaul/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge" ], "AINames": [ "Viridomarus", "Brennus", "Cativolcus", "Cingetorix", "Vercingetorix", "Divico", "Ambiorix", "Liscus", "Valetiacus", "Viridovix" ], "SkirmishReplacements": { "skirmish/structures/default_house_5": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/kush.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/kush.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/kush.json (revision 26298) @@ -1,94 +1,91 @@ { "Code": "kush", "Culture": "egyptian", - "Name": "Kushites", - "Emblem": "session/portraits/emblems/emblem_kushites.png", - "History": "The Kingdom of Kush was an ancient African kingdom situated on the confluences of the Blue Nile, White Nile and River Atbara in what is now the Republic of Sudan. The Kushite era of rule in the region was established after the Bronze Age collapse of the New Kingdom of Egypt, and it was centered at Napata in its early phase. They invaded Egypt in the 8th century BC, and the Kushite emperors ruled as Pharaohs of the Twenty-fifth dynasty of Egypt for a century, until they were expelled by the Assyrians. Kushite culture was influenced heavily by the Egyptians, with Kushite pyramid building and monumental temple architecture still extent. The Kushites even worshipped many Egyptian gods, including Amun. During Classical antiquity, the Kushite imperial capital was at Meroe. In early Greek geography, the Meroitic kingdom was known as Aethiopia. The Kushite kingdom persisted until the 4th century AD, when it weakened and disintegrated due to internal rebellion, eventually succumbing to the rising power of Axum.", "Music": [ { "File": "Ammon-Ra.ogg", "Type": "peace" }, { "File": "Sands_of_Time.ogg", "Type": "peace" }, { "File": "Land_between_the_two_Seas.ogg", "Type": "peace" }, { "File": "Valley_of_the_Nile.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Large Rams", "History": "", "Description": "Battering Rams +20% attack damage and +2 garrison capacity." } ], "StartEntities": [ { "Template": "structures/kush/civil_centre" }, { "Template": "units/kush/support_female_citizen", "Count": 4 }, { "Template": "units/kush/infantry_spearman_b", "Count": 2 }, { "Template": "units/kush/infantry_archer_b", "Count": 2 }, { "Template": "units/kush/cavalry_javelineer_b" }, { "Template": "units/kush/support_healer_b" } ], "WallSets": [ "structures/wallset_palisade", "structures/kush/wallset_stone" ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/syntagma" ], "AINames": [ "Kashta", "Alara", "Pebatjma", "Shabaka", "Shebitku", "Qalhata", "Takahatenamun", "Tantamani", "Atlanersa", "Nasalsa", "Malewiebamani", "Harsiotef", "Shanakdakhete", "Amanishakheto" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/kush/infantry_archer_b", "skirmish/units/special_starting_unit": "units/kush/support_healer_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json (revision 26298) @@ -1,84 +1,81 @@ { "Code": "maur", "Culture": "maur", - "Name": "Mauryas", - "Emblem": "session/portraits/emblems/emblem_mauryas.png", - "History": "Founded in 322 B.C. by Chandragupta Maurya, the Mauryan Empire was the first to rule most of the Indian subcontinent, and was one of the largest and most populous empires of antiquity. Its military featured bowmen who used the long-range bamboo longbow, fierce female warriors, chariots, and thousands of armored war elephants. Its philosophers, especially the famous Acharya Chanakya, contributed to such varied fields such as economics, religion, diplomacy, warfare, and good governance. Under the rule of Ashoka the Great, the empire saw 40 years of peace, harmony, and prosperity.", "Music": [ { "File": "An_old_Warhorse_goes_to_Pasture.ogg", "Type": "peace" }, { "File": "Land_between_the_two_Seas.ogg", "Type": "peace" }, { "File": "Eastern_Dreams.ogg", "Type": "peace" }, { "File": "Karmic_Confluence.ogg", "Type": "peace" } ], "CivBonuses": [], "WallSets": [ "structures/wallset_palisade", "structures/maur/wallset_stone" ], "StartEntities": [ { "Template": "structures/maur/civil_centre" }, { "Template": "units/maur/support_female_citizen", "Count": 4 }, { "Template": "units/maur/infantry_spearman_b", "Count": 2 }, { "Template": "units/maur/infantry_archer_b", "Count": 2 }, { "Template": "units/maur/cavalry_javelineer_b", "Count": 1 }, { "Template": "units/maur/support_elephant", "Count": 1 } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge" ], "AINames": [ "Chandragupta Maurya", "Bindusara Maurya", "Ashoka the Great", "Dasharatha Maurya", "Samprati Maurya", "Shalishuka Maurya", "Devavarman Maurya", "Shatadhanvan Maurya", "Brihadratha Maurya" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/maur/infantry_archer_b", "skirmish/units/special_starting_unit": "units/maur/support_elephant", "skirmish/structures/default_house_5": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json (revision 26298) @@ -1,98 +1,95 @@ { "Code": "ptol", "Culture": "ptol", - "Name": "Ptolemies", - "Emblem": "session/portraits/emblems/emblem_ptolemies.png", - "History": "The Ptolemaic dynasty was a Macedonian Greek royal family which ruled the Ptolemaic Empire in Egypt during the Hellenistic period. Their rule lasted for 275 years, from 305 BC to 30 BC. They were the last dynasty of ancient Egypt.", "Music": [ { "File": "Ammon-Ra.ogg", "Type": "peace" }, { "File": "Sands_of_Time.ogg", "Type": "peace" }, { "File": "Land_between_the_two_Seas.ogg", "Type": "peace" }, { "File": "Valley_of_the_Nile.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Polybolos", "History": "", "Description": "Bolt Shooters −33% attack damage and −33% attack time." } ], "WallSets": [ "structures/wallset_palisade", "structures/ptol/wallset_stone" ], "StartEntities": [ { "Template": "structures/ptol/civil_centre" }, { "Template": "units/ptol/support_female_citizen", "Count": 4 }, { "Template": "units/ptol/infantry_pikeman_b", "Count": 2 }, { "Template": "units/ptol/infantry_slinger_b", "Count": 2 }, { "Template": "units/ptol/cavalry_archer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Ptolemy Soter", "Ptolemy Philadelphus", "Ptolemy Epigone", "Ptolemy Eurgetes", "Ptolemy Philopater", "Ptolemy Epiphanes", "Ptolemy Philometor", "Ptolemy Eupator", "Ptolemy Alexander", "Ptolemy Neos Dionysos", "Ptolemy Neos Philopater", "Berenice Philopater", "Cleopatra Tryphaena", "Berenice Epiphaneia", "Cleopatra Philopater", "Cleopatra Selene", "Cleopatra II Philometora Soteira", "Arsinoe IV", "Arsinoe II" ], "SkirmishReplacements": { "skirmish/units/default_infantry_melee_b": "units/ptol/infantry_pikeman_b", "skirmish/units/default_infantry_ranged_b": "units/ptol/infantry_slinger_b", "skirmish/units/default_cavalry": "units/ptol/cavalry_archer_b", "skirmish/structures/default_house_5": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json (revision 26298) @@ -1,96 +1,93 @@ { "Code": "sele", "Culture": "sele", - "Name": "Seleucids", - "Emblem": "session/portraits/emblems/emblem_seleucids.png", - "History": "The Macedonian-Greek dynasty that ruled most of Alexander's former empire.", "Music": [ { "File": "Rise_of_Macedon.ogg", "Type": "peace" }, { "File": "In_the_Shadow_of_Olympus.ogg", "Type": "peace" }, { "File": "The_Hellespont.ogg", "Type": "peace" } ], "CivBonuses": [], "WallSets": [ "structures/wallset_palisade", "structures/sele/wallset_stone" ], "StartEntities": [ { "Template": "structures/sele/civil_centre" }, { "Template": "units/sele/support_female_citizen", "Count": 4 }, { "Template": "units/sele/infantry_spearman_b", "Count": 2 }, { "Template": "units/sele/infantry_javelineer_b", "Count": 2 }, { "Template": "units/sele/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Seleucus I Nicator", "Antiochus I Soter", "Antiochus II Theos", "Seleucus II Callinicus", "Seleucus III Ceraunus", "Antiochus III Megas", "Seleucus IV Philopator", "Antiochus IV Epiphanes", "Antiochus V Eupator", "Demetrius I Soter", "Alexander I Balas", "Demetrius II Nicator", "Antiochus VI Dionysus", "Diodotus Tryphon", "Antiochus VII Sidetes", "Demetrius II Nicator", "Alexander II Zabinas", "Cleopatra Thea", "Seleucus V Philometor", "Antiochus VIII Grypus", "Antiochus IX Cyzicenus", "Seleucus VI Epiphanes", "Antiochus X Eusebes", "Demetrius III Eucaerus", "Antiochus XI Epiphanes", "Philip I Philadelphus", "Antiochus XII Dionysus", "Seleucus VII Kybiosaktes", "Antiochus XIII Asiaticus", "Philip II Philoromaeus" ], "SkirmishReplacements": { "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml (revision 26298) @@ -0,0 +1,120 @@ + + + + 80 + 160 + 60000 + + + 200 + 12 + 0.04 + 4 + 0 + 8 + + + + 50 + 1 + 10 + 0 + 1 + 1 + 1 + 1 + 5 + 2 + 0 + 2 + 4 + 1 + 1 + 1 + 30 + 20 + 1 + + + + 15 + + + 4 + + + 5 + + + + + phase_town + + + + + Player + true + + + + + 1.0 + 1.0 + 1.0 + 1.0 + + + 1.0 + 1.0 + 1.0 + 1.0 + + + unlock_shared_los + unlock_shared_dropsites + 1.0 + + + + 0.0 + 0.0 + 0.0 + 0.0 + + 1000 + + + + interface/alarm/alarm_defeated.xml + interface/alarm/alarm_defeated_ally.xml + interface/alarm/alarm_defeated_enemy.xml + interface/alarm/alarm_no_idle_unit.xml + + + + + + Cavalry + Champion + Domestic + FemaleCitizen + Hero + Infantry + Ship + Siege + Trader + Worker + + + Economic + CivCentre + Fortress + House + Military + Outpost + Wonder + + + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/cart.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/cart.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/cart.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/cart_player_teambonus + + + cart + Carthaginians + Carthage, a city-state in modern-day Tunisia, was a formidable force in the western Mediterranean, eventually taking over much of North Africa and modern-day Spain in the third century B.C. The sailors of Carthage were among the fiercest contenders on the high seas, and masters of naval trade. They deployed towered War Elephants on the battlefield to fearsome effect, and had defensive walls so strong, they were never breached. + session/portraits/emblems/emblem_carthaginians.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/cart.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/iber.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/iber.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/iber.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/iber_player_teambonus + + + iber + Iberians + The Iberians were a people of mysterious origins and language, with a strong tradition of horsemanship and metalworking. A relatively peaceful culture, they usually fought in other's battles only as mercenaries. However, they proved tenacious when Rome sought to take their land and freedom from them, and employed pioneering guerrilla tactics and flaming javelins as they fought back. + session/portraits/emblems/emblem_iberians.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/iber.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 26298) @@ -1,626 +1,633 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ - "Code", "Culture", "Name", "Emblem", "History", "Music", "CivBonuses", "StartEntities", + "Code", "Culture", "Music", "CivBonuses", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); - if (!selectableOnly || data.SelectableInGameSetup) - civData[data.Code] = data; + if (selectableOnly && !data.SelectableInGameSetup) + continue; + + const template = Engine.GetTemplate("special/players/" + data.Code); + data.Name = template.Identity.GenericName; + data.Emblem = template.Identity.Icon; + data.History = template.Identity.History; + + civData[data.Code] = data; } return civData; } /** * @return {string[]} - All the classes for this identity template. */ function GetIdentityClasses(template) { let classString = ""; if (template.Classes && template.Classes._string) classString += " " + template.Classes._string; if (template.VisibleClasses && template.VisibleClasses._string) classString += " " + template.VisibleClasses._string; if (template.Rank) classString += " " + template.Rank; return classString.length > 1 ? classString.substring(1).split(" ") : []; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity classes. * * @param classes - List of the classes to check against. * @param match - Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2. * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed. * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed. * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise. */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // Transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (let sublist of match) { // If the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {Object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Resistance) { // Don't show Foundation resistance. ret.resistance = {}; if (template.Resistance.Entity) { if (template.Resistance.Entity.Damage) { ret.resistance.Damage = {}; for (let damageType in template.Resistance.Entity.Damage) ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType); } if (template.Resistance.Entity.Capture) ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture"); if (template.Resistance.Entity.ApplyStatus) { ret.resistance.ApplyStatus = {}; for (let statusEffect in template.Resistance.Entity.ApplyStatus) ret.resistance.ApplyStatus[statusEffect] = { "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"), "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") }; } } } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } if (temp.ApplyStatus) effects.ApplyStatus = temp.ApplyStatus; return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "attackName": { "name": template.Attack[type].AttackName._string || template.Attack[type].AttackName, "context": template.Attack[type].AttackName["@context"] }, "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "yOrigin": getAttackStat("Origin/Y") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].yOrigin + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Projectile) ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]); } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) { ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category }; if (template.TrainingRestrictions.MatchLimit) ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit; } if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.Garrisonable) ret.garrisonable = { "size": getEntityValue("Garrisonable/Size") }; if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "health": getEntityValue("Heal/Health"), "range": getEntityValue("Heal/Range"), "interval": getEntityValue("Heal/Interval") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceDropsite) ret.resourceDropsite = { "types": template.ResourceDropsite.Types.split(" ") }; if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Population && template.Population.Bonus) ret.population = { "bonus": getEntityValue("Population/Bonus") }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { const walkSpeed = getEntityValue("UnitMotion/WalkSpeed"); ret.speed = { "walk": walkSpeed, "run": walkSpeed, "acceleration": getEntityValue("UnitMotion/Acceleration") }; if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.Researcher) { ret.techCostMultiplier = {}; for (const res in template.Researcher.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.Treasure) { ret.treasure = { "collectTime": getEntityValue("Treasure/CollectTime"), "resources": {} }; for (let resource in template.Treasure.Resources) ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource); } if (template.TurretHolder) ret.turretHolder = { "turretPoints": template.TurretHolder.TurretPoints }; if (template.Upkeep) { ret.upkeep = { "interval": +template.Upkeep.Interval, "rates": {} }; for (let type in template.Upkeep.Rates) ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type); } if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "/fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } /** * Get information about an aura template. * @param {object} template - A valid template as obtained by loading the aura JSON file. */ function GetAuraDataHelper(template) { return { "name": { "generic": template.auraName, }, "description": template.auraDescription || null, "modifications": template.modifications, "radius": template.radius || null, }; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 26298) @@ -1,340 +1,336 @@ /** * This class handles the loading of files. */ class TemplateLoader { constructor() { /** * Raw Data Caches. */ this.auraData = {}; this.playerData = {}; this.technologyData = {}; this.templateData = {}; /** * Partly-composed data. */ this.autoResearchTechList = this.findAllAutoResearchedTechs(); } /** * Loads raw aura template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {Object} Object containing raw template data. */ loadAuraTemplate(templateName) { if (!(templateName in this.auraData)) { let data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json"); translateObjectKeys(data, this.AuraTranslateKeys); this.auraData[templateName] = data; } return this.auraData[templateName]; } /** * Loads raw entity template. * * Loads from local cache if data present, else from file system. * * @param {string} templateName * @param {string} civCode * @return {Object} Object containing raw template data. */ loadEntityTemplate(templateName, civCode) { if (!(templateName in this.templateData)) { // We need to clone the template because we want to perform some translations. let data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, this.EntityTranslateKeys); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) this.loadAuraTemplate(auraID); if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode) warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " + "This does not match the currently selected civ \"" + civCode + "\"."); this.templateData[templateName] = data; } return this.templateData[templateName]; } /** * Loads raw player template. * * Loads from local cache if data present, else from file system. * * If a civ doesn't have their own civ-specific template, * then we return the generic template. * * @param {string} civCode * @return {Object} Object containing raw template data. */ loadPlayerTemplate(civCode) { if (!(civCode in this.playerData)) { let templateName = this.buildPlayerTemplateName(civCode); this.playerData[civCode] = Engine.GetTemplate(templateName); // No object keys need to be translated } return this.playerData[civCode]; } /** * Loads raw technology template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {Object} Object containing raw template data. */ loadTechnologyTemplate(templateName) { if (!(templateName in this.technologyData)) { let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json"); translateObjectKeys(data, this.TechnologyTranslateKeys); // Translate specificName as in GetTechnologyData() from gui/session/session.js if (typeof (data.specificName) === 'object') for (let civ in data.specificName) data.specificName[civ] = translate(data.specificName[civ]); else if (data.specificName) warn("specificName should be an object of civ->name mappings in " + templateName + ".json"); this.technologyData[templateName] = data; } return this.technologyData[templateName]; } /** * @param {string} templateName * @param {string} civCode * @return {Object} Contains a list and the requirements of the techs in the pair */ loadTechnologyPairTemplate(templateName, civCode) { let template = this.loadTechnologyTemplate(templateName); return { "techs": [template.top, template.bottom], "reqs": DeriveTechnologyRequirements(template, civCode) }; } deriveProduction(template, civCode) { const production = { "techs": [], "units": [] }; if (!template.Researcher && !template.Trainer) return production; if (template.Trainer?.Entities?._string) for (let templateName of template.Trainer.Entities._string.split(" ")) { templateName = templateName.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(templateName)) production.units.push(templateName); } const appendTechnology = (technologyName) => { const technology = this.loadTechnologyTemplate(technologyName, civCode); if (DeriveTechnologyRequirements(technology, civCode)) production.techs.push(technologyName); }; if (template.Researcher?.Technologies?._string) for (let technologyName of template.Researcher.Technologies._string.split(" ")) { if (technologyName.indexOf("{civ}") != -1) { const civTechName = technologyName.replace("{civ}", civCode); technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); } if (this.isPairTech(technologyName)) { let technologyPair = this.loadTechnologyPairTemplate(technologyName, civCode); if (technologyPair.reqs) for (technologyName of technologyPair.techs) appendTechnology(technologyName); } else appendTechnology(technologyName); } return production; } deriveBuildQueue(template, civCode) { let buildQueue = []; if (!template.Builder || !template.Builder.Entities._string) return buildQueue; for (let build of template.Builder.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(build)) buildQueue.push(build); } return buildQueue; } deriveModifications(civCode, auraList) { const modificationData = []; for (const techName of this.autoResearchTechList) modificationData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode)); for (const auraName of auraList) modificationData.push(this.loadAuraTemplate(auraName)); return DeriveModificationsFromTechnologies(modificationData); } /** * If a civ doesn't have its own civ-specific player template, * this returns the name of the generic player template. * * @see simulation/helpers/Player.js GetPlayerTemplateName() * (Which can't be combined with this due to different Engine contexts) */ buildPlayerTemplateName(civCode) { - let templateName = this.PlayerPath + this.PlayerTemplatePrefix + civCode; + let templateName = this.PlayerPath + civCode; if (Engine.TemplateExists(templateName)) return templateName; - return this.PlayerPath + this.PlayerTemplateFallback; + warn("No template found for civ " + civCode + "."); + + return this.PlayerPath + this.DefaultCiv; } /** * Crudely iterates through every tech JSON file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ findAllAutoResearchedTechs() { let techList = []; for (let templateName of listFiles(this.TechnologyPath, ".json", true)) { let data = this.loadTechnologyTemplate(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } /** * A template may be a variant of another template, * eg. `*_house`, `*_trireme`, or a promotion. * * This method returns an array containing: * [0] - The template's basename * [1] - The variant type * [2] - Further information (if available) * * e.g.: * units/athen/infantry_swordsman_e * -> ["units/athen/infantry_swordsman_b", TemplateVariant.promotion, "elite"] * * units/brit/support_female_citizen_house * -> ["units/brit/support_female_citizen", TemplateVariant.unlockedByTechnology, "unlock_female_house"] */ getVariantBaseAndType(templateName, civCode) { if (!templateName || !Engine.TemplateExists(templateName)) return undefined; templateName = removeFiltersFromTemplateName(templateName); let template = this.loadEntityTemplate(templateName, civCode); if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName)) return [templateName, TemplateVariant.base]; let parentTemplate = this.loadEntityTemplate(template["@parent"], civCode); let inheritedVariance = this.getVariantBaseAndType(template["@parent"], civCode); if (parentTemplate.Identity) { if (parentTemplate.Identity.Civ && parentTemplate.Identity.Civ != template.Identity.Civ) return [templateName, TemplateVariant.base]; if (parentTemplate.Identity.Rank && parentTemplate.Identity.Rank != template.Identity.Rank) return [inheritedVariance[0], TemplateVariant.promotion, template.Identity.Rank.toLowerCase()]; } if (parentTemplate.Upgrade) for (let upgrade in parentTemplate.Upgrade) if (parentTemplate.Upgrade[upgrade].Entity) return [inheritedVariance[0], TemplateVariant.upgrade, upgrade.toLowerCase()]; if (template.Identity.RequiredTechnology) return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.RequiredTechnology]; if (parentTemplate.Cost) for (let res in parentTemplate.Cost.Resources) if (+parentTemplate.Cost.Resources[res]) return [inheritedVariance[0], TemplateVariant.trainable]; warn("Template variance unknown: " + templateName); return [templateName, TemplateVariant.unknown]; } isPairTech(technologyCode) { return !!this.loadTechnologyTemplate(technologyCode).top; } isPhaseTech(technologyCode) { return basename(technologyCode).startsWith("phase"); } } /** * Paths to certain files. * * It might be nice if we could get these from somewhere, instead of having them hardcoded here. */ TemplateLoader.prototype.AuraPath = "simulation/data/auras/"; -TemplateLoader.prototype.PlayerPath = "special/player/"; +TemplateLoader.prototype.PlayerPath = "special/players/"; TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/"; TemplateLoader.prototype.DefaultCiv = "gaia"; /** - * Expected prefix for player templates, and the file to use if a civ doesn't have its own. - */ -TemplateLoader.prototype.PlayerTemplatePrefix = "player_"; -TemplateLoader.prototype.PlayerTemplateFallback = "player"; - -/** * Keys of template values that are to be translated on load. */ TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"]; TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"]; Index: ps/trunk/binaries/data/mods/public/l10n/messages.json =================================================================== --- ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 26298) @@ -1,896 +1,905 @@ [ { "output": "public-civilizations.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "json", "filemasks": [ "simulation/data/civs/**.json" ], "options": { "keywords": [ - "Name", - "Description", - "History", "Special", "AINames" ] } + }, + { + "extractor": "xml", + "filemasks": [ + "simulation/templates/special/players/**.xml" + ], + "options": { + "keywords": [ + "GenericName": {}, + "History": {} + ] + } } ] }, { "output": "public-gui-ingame.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "gui/session/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "gui/session/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } } ] }, { "output": "public-gui-gamesetup.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "gui/gamesettings/attributes/**.js", "gui/gamesetup/**.js", "gui/gamesetup_mp/**.js", "gui/loading/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "gui/gamesetup/**.xml", "gui/gamesetup_mp/**.xml", "gui/loading/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } }, { "extractor": "txt", "filemasks": [ "gui/text/quotes.txt" ], "options": { } } ] }, { "output": "public-gui-lobby.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "gui/lobby/**.js", "gui/prelobby/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "gui/lobby/**.xml", "gui/prelobby/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } }, { "extractor": "txt", "filemasks": [ "gui/prelobby/common/terms/*.txt" ], "options": { } } ] }, { "output": "public-gui-manual.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "gui/manual/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "gui/manual/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } }, { "extractor": "txt", "filemasks": [ "gui/manual/intro.txt" ], "options": { } } ] }, { "output": "public-gui-userreport.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "txt", "filemasks": [ "gui/userreport/**.txt" ], "options": { } } ] }, { "output": "public-gui-campaigns.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "gui/campaigns/**.js", "gui/common/campaigns/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "gui/campaigns/**.xml", "gui/common/campaigns/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } }, { "extractor": "json", "filemasks": [ "campaigns/**.json" ], "options": { "keywords": [ "Name", "Description" ], "context": "Campaign Template" } } ] }, { "output": "public-gui-other.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "globalscripts/**.js", "gui/common/**.js", "gui/credits/**.js", "gui/hotkeys/**.js", "gui/loadgame/**.js", "gui/locale/**.js", "gui/locale_advanced/**.js", "gui/maps/**.js", "gui/options/**.js", "gui/pregame/**.js", "gui/reference/**.js", "gui/replaymenu/**.js", "gui/splashscreen/**.js", "gui/summary/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "dennis-ignore:", "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "globalscripts/**.xml", "gui/common/**.xml", "gui/credits/**.xml", "gui/hotkeys/**.xml", "gui/loadgame/**.xml", "gui/locale/**.xml", "gui/locale_advanced/**.xml", "gui/maps/**.xml", "gui/options/**.xml", "gui/pregame/**.xml", "gui/reference/**.xml", "gui/replaymenu/**.xml", "gui/splashscreen/**.xml", "gui/summary/**.xml" ], "options": { "keywords": { "translatableAttribute": { "locationAttributes": ["id"] }, "translate": {} } } }, { "extractor": "json", "filemasks": [ "gui/credits/texts/**.json" ], "options": { "keywords": [ "Title", "Subtitle" ] } }, { "extractor": "json", "filemasks": [ "gui/hotkeys/spec/**.json" ], "options": { "keywords": [ "name", "desc" ], "context": "hotkey metadata" } }, { "extractor": "json", "filemasks": [ "gui/options/**.json" ], "options": { "keywords": [ "label", "tooltip" ] } }, { "extractor": "json", "filemasks": [ "simulation/data/resources/**.json" ], "options": { "keywords": [ "description" ] } }, { "extractor": "json", "filemasks": [ "simulation/data/resources/**.json" ], "options": { "keywords": [ "name", "subtypes" ], "comments": [ "Translation: Word as used at the beginning of a sentence or as a single-word sentence." ], "context": "firstWord" } }, { "extractor": "json", "filemasks": [ "simulation/data/resources/**.json" ], "options": { "keywords": [ "name", "subtypes" ], "comments": [ "Translation: Word as used in the middle of a sentence (which may require using lowercase for your language)." ], "context": "withinSentence" } }, { "extractor": "txt", "filemasks": [ "gui/gamesetup/**.txt", "gui/splashscreen/splashscreen.txt", "gui/text/tips/**.txt" ], "options": { } } ] }, { "output": "public-templates-units.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "xml", "filemasks": [ "simulation/templates/template_unit_*.xml", "simulation/templates/units/**.xml" ], "options": { "keywords": { "AttackName": { "customContext": "Name of an attack, usually the weapon." }, "StatusName": { "customContext": "status effect" }, "ApplierTooltip": { "customContext": "status effect" }, "ReceiverTooltip": { "customContext": "status effect" }, "GenericName": {}, "SpecificName": {}, "History": {}, "VisibleClasses": { "splitOnWhitespace": true }, "Tooltip": {}, "DisabledTooltip": {}, "FormationName": {}, "FromClass": {}, "Shape": {}, "Rank": { "tagAsContext": true } } } } ] }, { "output": "public-templates-buildings.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "xml", "filemasks": [ "simulation/templates/template_structure_*.xml", "simulation/templates/structures/**.xml" ], "options": { "keywords": { "AttackName": { "customContext": "Name of an attack, usually the weapon." }, "StatusName": { "customContext": "status effect" }, "ApplierTooltip": { "customContext": "status effect" }, "ReceiverTooltip": { "customContext": "status effect" }, "GenericName": {}, "SpecificName": {}, "History": {}, "VisibleClasses": { "splitOnWhitespace": true }, "Tooltip": {}, "DisabledTooltip": {}, "FormationName": {}, "FromClass": {}, "Rank": { "tagAsContext": true } } } } ] }, { "output": "public-templates-other.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "xml", "filemasks": { "includeMasks": [ "simulation/templates/**.xml" ], "excludeMasks": [ "simulation/templates/structures/**.xml", "simulation/templates/template_structure_*.xml", "simulation/templates/template_unit_*.xml", "simulation/templates/units/**.xml" ] }, "options": { "keywords": { "AttackName": { "customContext": "Name of an attack, usually the weapon." }, "GenericName": {}, "SpecificName": {}, "History": {}, "VisibleClasses": { "splitOnWhitespace": true }, "Tooltip": {}, "DisabledTooltip": {}, "FormationName": {}, "FromClass": {}, "Rank": { "tagAsContext": true } } } }, { "extractor": "json", "filemasks": [ "simulation/data/damage_types/*.json" ], "options": { "keywords": [ "name", "description" ], "context": "damage type" } }, { "extractor": "json", "filemasks": [ "simulation/data/status_effects/*.json" ], "options": { "keywords": [ "statusName", "applierTooltip", "receiverTooltip" ], "context": "status effect" } }, { "extractor": "json", "filemasks": [ "simulation/data/attack_effects/*.json" ], "options": { "keywords": [ "name", "description" ], "context": "effect caused by an attack" } } ] }, { "output": "public-simulation-auras.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "json", "filemasks": [ "simulation/data/auras/**.json" ], "options": { "keywords": [ "auraName", "auraDescription" ] } } ] }, { "output": "public-simulation-technologies.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "json", "filemasks": [ "simulation/data/technologies/**.json" ], "options": { "keywords": [ "specificName", "genericName", "description", "tooltip", "requirementsTooltip" ] } } ] }, { "output": "public-simulation-other.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "simulation/ai/**.js", "simulation/components/**.js", "simulation/helpers/**.js" ], "options": { "format": "javascript-format", "keywords": { "translate": [1], "translatePlural": [1, 2], "translateWithContext": [[1], 2], "translatePluralWithContext": [[1], 2, 3], "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/player_defaults.json" ], "options": { "keywords": [ "Name" ] } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/game_speeds.json" ], "options": { "keywords": ["Title"] } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/victory_conditions/*.json" ], "options": { "keywords": ["Title", "Description"] } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/starting_resources.json" ], "options": { "keywords": ["Title"], "context": "startingResources" } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/trigger_difficulties.json" ], "options": { "keywords": ["Title", "Tooltip"] } }, { "extractor": "json", "filemasks": [ "simulation/data/settings/map_sizes.json" ], "options": { "keywords": [ "Name", "Tooltip" ] } }, { "extractor": "json", "filemasks": [ "simulation/ai/**.json" ], "options": { "keywords": [ "name", "description" ] } } ] }, { "output": "public-maps.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "json", "filemasks": { "includeMasks": [ "maps/random/**.json" ], "excludeMasks": [ "maps/random/rmbiome/**.json" ] }, "options": { "keywords": [ "Name", "Description" ] } }, { "extractor": "javascript", "filemasks": [ "maps/scenarios/**.js", "maps/skirmishes/**.js", "maps/random/**.js", "maps/scripts/**.js" ], "options": { "format": "javascript-format", "keywords": { "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "maps/scenarios/**.xml", "maps/skirmishes/**.xml" ], "options": { "keywords": { "ScriptSettings": { "extractJson": { "keywords": [ "Name", "Description" ] } } } } }, { "extractor": "json", "filemasks": [ "maps/random/rmbiome/**.json" ], "options": { "keywords": ["Description"], "context": "biome definition" } } ] }, { "output": "public-tutorials.pot", "inputRoot": "..", "project": "0 A.D. — Empires Ascendant", "copyrightHolder": "Wildfire Games", "rules": [ { "extractor": "javascript", "filemasks": [ "maps/tutorials/**.js" ], "options": { "format": "javascript-format", "keywords": { "markForTranslation": [1], "markForTranslationWithContext": [[1], 2], "markForPluralTranslation": [1, 2] }, "commentTags": [ "Translation:" ] } }, { "extractor": "xml", "filemasks": [ "maps/tutorials/**.xml" ], "options": { "keywords": { "ScriptSettings": { "extractJson": { "keywords": [ "Name", "Description" ] } } } } } ] } ] Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_Constraint.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_Constraint.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_Constraint.js (revision 26298) @@ -1,21 +1,31 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); var g_MapSettings = { "Size": 512 }; var g_Map = new RandomMap(0, 0, "blackness"); { let tileClass = new TileClass(g_Map.getSize()); let addedPos = new Vector2D(5, 0); tileClass.add(addedPos); let origin = new Vector2D(0, 0); TS_ASSERT(!(new AvoidTileClassConstraint(tileClass, 0).allows(addedPos))); TS_ASSERT(new AvoidTileClassConstraint(tileClass, 0).allows(origin)); TS_ASSERT(!(new AvoidTileClassConstraint(tileClass, 5).allows(origin))); TS_ASSERT(new NearTileClassConstraint(tileClass, 5).allows(origin)); TS_ASSERT(new NearTileClassConstraint(tileClass, 20).allows(origin)); TS_ASSERT(!(new NearTileClassConstraint(tileClass, 4).allows(origin))); } Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_DiskPlacer.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_DiskPlacer.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_DiskPlacer.js (revision 26298) @@ -1,63 +1,73 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); { var g_MapSettings = { "Size": 512 }; var g_Map = new RandomMap(0, 0, "blackness"); let center = new Vector2D(10, 10); let area = createArea(new DiskPlacer(3, center)); // Contains center TS_ASSERT(area.contains(center)); // Contains disk boundaries TS_ASSERT(area.contains(new Vector2D(10, 13))); TS_ASSERT(area.contains(new Vector2D(10, 7))); TS_ASSERT(area.contains(new Vector2D(7, 10))); TS_ASSERT(area.contains(new Vector2D(13, 10))); // Does not contain rectangle vertices TS_ASSERT(!area.contains(new Vector2D(13, 13))); TS_ASSERT(!area.contains(new Vector2D(7, 7))); TS_ASSERT(!area.contains(new Vector2D(13, 7))); TS_ASSERT(!area.contains(new Vector2D(7, 13))); // Does not contain points outside disk range TS_ASSERT(!area.contains(new Vector2D(10, 14))); TS_ASSERT(!area.contains(new Vector2D(10, 6))); TS_ASSERT(!area.contains(new Vector2D(6, 10))); TS_ASSERT(!area.contains(new Vector2D(14, 10))); area = createArea(new DiskPlacer(3, new Vector2D(0, 0))); // Does not allow points out of map boundaries TS_ASSERT(!area.contains(new Vector2D(-1, -1))); // Contains map edge TS_ASSERT(area.contains(new Vector2D(0, 0))); } { // Contains points outside map disk range on CircularMap var g_MapSettings = { "Size": 512, "CircularMap": true }; var g_Map = new RandomMap(0, 0, "blackness"); var area = createArea(new DiskPlacer(10, new Vector2D(436, 436))); TS_ASSERT(area.contains(new Vector2D(438, 438))); TS_ASSERT(area.contains(new Vector2D(437, 436))); TS_ASSERT(area.contains(new Vector2D(436, 437))); TS_ASSERT(area.contains(new Vector2D(435, 435))); area = createArea(new DiskPlacer(3, new Vector2D(0, 0))); // Does not allow points out of map boundaries TS_ASSERT(!area.contains(new Vector2D(-1, -1))); } { var g_MapSettings = { "Size": 320, "CircularMap": true }; var g_Map = new RandomMap(0, 0, "blackness"); // Does not error with floating point radius var area = createArea(new DiskPlacer(86.4, new Vector2D(160, 160))); // Does not error with extreme out of bounds disk area = createArea(new DiskPlacer(86.4, new Vector2D(800, 800))); // Does not error when disk on edge area = createArea(new DiskPlacer(10, new Vector2D(321, 321))); } Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_LayeredPainter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_LayeredPainter.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_LayeredPainter.js (revision 26298) @@ -1,22 +1,32 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); var g_MapSettings = { "Size": 512 }; var g_Map = new RandomMap(0, 0, "blackness"); { let min = new Vector2D(4, 4); let max = new Vector2D(10, 10); let center = Vector2D.average([min, max]); createArea( new RectPlacer(min, max), new LayeredPainter(["red", "blue"], [2])); TS_ASSERT_EQUALS(g_Map.getTexture(min), "red"); TS_ASSERT_EQUALS(g_Map.getTexture(max), "red"); TS_ASSERT_EQUALS(g_Map.getTexture(new Vector2D(-1, -1).add(max)), "red"); TS_ASSERT_EQUALS(g_Map.getTexture(new Vector2D(-2, -2).add(max)), "blue"); TS_ASSERT_EQUALS(g_Map.getTexture(new Vector2D(-3, -3).add(max)), "blue"); TS_ASSERT_EQUALS(g_Map.getTexture(center), "blue"); } Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_RectPlacer.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_RectPlacer.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_RectPlacer.js (revision 26298) @@ -1,17 +1,27 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); var g_MapSettings = { "Size": 512 }; var g_Map = new RandomMap(0, 0, "blackness"); { let min = new Vector2D(5, 5); let max = new Vector2D(7, 7); let area = createArea(new RectPlacer(min, max)); TS_ASSERT(!area.contains(new Vector2D(-1, -1).add(min))); TS_ASSERT(area.contains(min)); TS_ASSERT(area.contains(max)); TS_ASSERT(area.contains(max.clone())); TS_ASSERT(area.contains(Vector2D.average([min, max]))); } Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_SmoothingPainter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_SmoothingPainter.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_SmoothingPainter.js (revision 26298) @@ -1,30 +1,40 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); var g_MapSettings = { "Size": 512 }; var g_Map; { let min = new Vector2D(5, 5); let center = new Vector2D(6, 6); let max = new Vector2D(7, 7); let minHeight = 20; let maxHeight = 25; // Test SmoothingPainter { g_Map = new RandomMap(0, 0, "blackness"); let centerHeight = g_Map.getHeight(center); createArea( new RectPlacer(min, max), [ new RandomElevationPainter(minHeight, maxHeight), new SmoothingPainter(2, 1, 1) ]); TS_ASSERT_GREATER_EQUAL(g_Map.getHeight(center), centerHeight); TS_ASSERT_LESS_EQUAL(g_Map.getHeight(center), minHeight); } } Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_TileClass.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_TileClass.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_TileClass.js (revision 26298) @@ -1,55 +1,65 @@ +Engine.GetTemplate = (path) => { + return { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }; +}; + Engine.LoadLibrary("rmgen"); var g_MapSettings = { "Size": 512 }; var g_Map = new RandomMap(0, "blackness"); // Test that that it checks by value, not by reference { const tileClass = new TileClass(2); const reference1 = new Vector2D(1, 1); const reference2 = new Vector2D(1, 1); tileClass.add(reference1); TS_ASSERT(tileClass.has(reference2)); } // Test out-of-bounds { const tileClass = new TileClass(32); const absentPoints = [ new Vector2D(0, 0), new Vector2D(0, 1), new Vector2D(1, 0), new Vector2D(-1, -1), new Vector2D(2048, 0), new Vector2D(0, NaN), new Vector2D(0, Infinity) ]; for (const point of absentPoints) TS_ASSERT(!tileClass.has(point)); } // Test getters { const tileClass = new TileClass(88); const point = new Vector2D(5, 5); tileClass.add(point); const pointBorder = new Vector2D(1, 9); tileClass.add(pointBorder); TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 0), 1); TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 1), 1); TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 100), 2); TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4); TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 2), 12); TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 3), 28); // Points not on the map are not counted. TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 1), 4); TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 2), 11); TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 3), 22); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 26298) @@ -1,208 +1,208 @@ function Builder() {} Builder.prototype.Schema = "Allows the unit to construct and repair buildings." + "" + "1.0" + "" + "\n structures/{civ}/barracks\n structures/{native}/civil_centre\n structures/pers/apadana\n " + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + ""; /* * Build interval and repeat time, in ms. */ Builder.prototype.BUILD_INTERVAL = 1000; Builder.prototype.Init = function() { }; Builder.prototype.GetEntitiesList = function() { let string = this.template.Entities._string; if (!string) return []; let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return []; string = ApplyValueModificationsToEntity("Builder/Entities/_string", string, this.entity); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); - let entities = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); + const entities = string.replace(/\{civ\}/g, QueryOwnerInterface(this.entity, IID_Identity).GetCiv()).split(/\s+/); let disabledTemplates = cmpPlayer.GetDisabledTemplates(); let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); return entities.filter(ent => !disabledTemplates[ent] && cmpTemplateManager.TemplateExists(ent)); }; Builder.prototype.GetRange = function() { let max = 2; let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) max += cmpObstruction.GetSize(); return { "max": max, "min": 0 }; }; Builder.prototype.GetRate = function() { return ApplyValueModificationsToEntity("Builder/Rate", +this.template.Rate, this.entity); }; /** * @param {number} target - The target to check. * @return {boolean} - Whether we can build/repair the given target. */ Builder.prototype.CanRepair = function(target) { let cmpFoundation = QueryMiragedInterface(target, IID_Foundation); let cmpRepairable = QueryMiragedInterface(target, IID_Repairable); if (!cmpFoundation && (!cmpRepairable || !cmpRepairable.IsRepairable())) return false; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target); }; /** * @param {number} target - The target to repair. * @param {number} callerIID - The IID to notify on specific events. * @return {boolean} - Whether we started repairing. */ Builder.prototype.StartRepairing = function(target, callerIID) { if (this.target) this.StopRepairing(); if (!this.CanRepair(target)) return false; let cmpBuilderList = QueryBuilderListInterface(target); if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("build", false, 1.0); this.target = target; this.callerIID = callerIID; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Builder, "PerformBuilding", this.BUILD_INTERVAL, this.BUILD_INTERVAL, null); return true; }; /** * @param {string} reason - The reason why we stopped repairing. */ Builder.prototype.StopRepairing = function(reason) { if (!this.target) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; let cmpBuilderList = QueryBuilderListInterface(this.target); if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; if (reason && callerIID) { let component = Engine.QueryInterface(this.entity, callerIID); if (component) component.ProcessMessage(reason, null); } }; /** * Repair our target entity. * @params - data and lateness are unused. */ Builder.prototype.PerformBuilding = function(data, lateness) { if (!this.CanRepair(this.target)) { this.StopRepairing("TargetInvalidated"); return; } if (!this.IsTargetInRange(this.target)) { this.StopRepairing("OutOfRange"); return; } // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); let cmpFoundation = Engine.QueryInterface(this.target, IID_Foundation); if (cmpFoundation) { cmpFoundation.Build(this.entity, this.GetRate()); return; } let cmpRepairable = Engine.QueryInterface(this.target, IID_Repairable); if (cmpRepairable) { cmpRepairable.Repair(this.entity, this.GetRate()); return; } }; /** * @param {number} - The entity ID of the target to check. * @return {boolean} - Whether this entity is in range of its target. */ Builder.prototype.IsTargetInRange = function(target) { let range = this.GetRange(); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; Builder.prototype.OnValueModification = function(msg) { if (msg.component != "Builder" || !msg.valueNames.some(name => name.endsWith('_string'))) return; // Token changes may require selection updates. let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); }; Engine.RegisterComponentType(IID_Builder, "Builder", Builder); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 26298) @@ -1,2135 +1,2137 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized for the biggest part, // so most of the attributes shouldn't be serialized. // Return an object with a small selection of deterministic data. return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { - let cmpPlayer = QueryPlayerIDInterface(i); - let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); + const playerEnt = cmpPlayerManager.GetPlayerByID(i); + const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); + const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); + const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity); // Work out which phase we are in. let phase = ""; - let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager); + const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ - "name": cmpPlayer.GetName(), - "civ": cmpPlayer.GetCiv(), + "name": cmpIdentity.GetName(), + "civ": cmpIdentity.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui. */ GuiInterface.prototype.GetEntityState = function(player, ent) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; let ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "controllable": cmpIdentity.IsControllable() }; const cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) ret.formation = { "members": cmpFormation.GetMembers() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval") }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher); if (cmpResearcher) ret.researcher = { "technologies": cmpResearcher.GetTechnologiesList(), "techCostMultiplier": cmpResearcher.GetTechCostMultiplier() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); if (cmpTrainer) ret.trainer = { "entities": cmpTrainer.GetEntitiesList() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), "isIdle": cmpUnitAI.IsIdle(), "formations": cmpUnitAI.GetFormationsList(), "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked() }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type); let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(), "acceleration": cmpUnitMotion.GetAcceleration() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; const yOrigin = cmd.yOrigin || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, owner, aurasTemplate); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } return GetTemplateDataHelper(template, owner, aurasTemplate); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(data.tech); }; /** * Checks whether the requirements for this technology have been met. */ GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; /** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ GuiInterface.prototype.GetStartedResearch = function(player) { return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {}; }; /** * Returns the battle state of the player. */ GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; /** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default. if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // Filter on players and time, since the delete timer might be executed with a delay. return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Formation.FormationName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Formation.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); if (!cmpPlayer) continue; cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color. let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r": 1, "g": 1, "b": 1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetDisplayedColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them. for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities. for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // Entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location). let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner. let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position. let pos; if (cmd.x && cmd.z) pos = cmd; else // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location. let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // Do some entity cache management and check for snapping. if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // We're clearing the preview, clear the entity cache and bail. for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // Keep template data around. } return false; } for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before. for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // Prevent division by zero errors further on if the start and end positions are the same. if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // Clear the single-building preview entity (we'll be rolling our own). this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // Calculate wall placement and position preview entities. let result = { "pieces": [], "cost": { "population": 0, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group. let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true // Preview only, must not appear in the result. }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above. if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || []; previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup()); } // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group. let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // Move piece to right location. // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities. let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces. if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region. // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta. let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement. validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest. // (TODO: Break unlikely ties by choosing the lowest entity ID.) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. let bucket = filtered.bucket; if (bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) return { "idle": false }; const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); if (cmpTurretable && cmpTurretable.IsTurreted()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if (!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; else if (!firstMarket) result = { "type": "set first" }; else if (!secondMarket) result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; else result = { "type": "set first" }; return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0; }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be * appropriately careful. They are called with a first argument "player", which is * trusted and indicates the player associated with the current client; no data should * be returned unless this player is meant to be able to see it.) */ let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, "ResetTemplateModified": 1, "IsSelectionDirty": 1, "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \"" + name + "\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 26298) @@ -1,204 +1,220 @@ function Identity() {} Identity.prototype.Schema = "Specifies various names and values associated with the entity, typically for GUI display to users." + "" + "athen" + "Athenian Hoplite" + "Hoplī́tēs Athēnaïkós" + "units/athen_infantry_spearman.png" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "Basic" + "Advanced" + "Elite" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Identity.prototype.Init = function() { this.classesList = GetIdentityClasses(this.template); this.visibleClassesList = GetVisibleIdentityClasses(this.template); if (this.template.Phenotype) this.phenotype = pickRandom(this.GetPossiblePhenotypes()); else this.phenotype = "default"; this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; }; Identity.prototype.GetCiv = function() { return this.template.Civ; }; Identity.prototype.GetLang = function() { return this.template.Lang || "greek"; // ugly default }; /** * Get a list of possible Phenotypes. * @return {string[]} A list of possible phenotypes. */ Identity.prototype.GetPossiblePhenotypes = function() { return this.template.Phenotype._string.split(/\s+/); }; /** * Get the current Phenotype. * @return {string} The current phenotype. */ Identity.prototype.GetPhenotype = function() { return this.phenotype; }; Identity.prototype.GetRank = function() { return this.template.Rank || ""; }; Identity.prototype.GetClassesList = function() { return this.classesList; }; Identity.prototype.GetVisibleClassesList = function() { return this.visibleClassesList; }; Identity.prototype.HasClass = function(name) { return this.GetClassesList().indexOf(name) != -1; }; Identity.prototype.GetSelectionGroupName = function() { return this.template.SelectionGroupName || ""; }; Identity.prototype.GetGenericName = function() { return this.template.GenericName; }; Identity.prototype.IsUndeletable = function() { return this.template.Undeletable == "true"; }; Identity.prototype.IsControllable = function() { return this.controllable; }; Identity.prototype.SetControllable = function(controllability) { this.controllable = controllability; }; Identity.prototype.SetPhenotype = function(phenotype) { this.phenotype = phenotype; }; +/** + * @param {string} newName - + */ +Identity.prototype.SetName = function(newName) +{ + this.name = newName; +}; + +/** + * @return {string} - + */ +Identity.prototype.GetName = function() +{ + return this.name || this.template.GenericName; +}; + function IdentityMirage() {} IdentityMirage.prototype.Init = function(cmpIdentity) { // Mirages don't get identity classes via the template-filter, so that code can query // identity components via Engine.QueryInterface without having to explicitly check for mirages. // This is cloned as otherwise we get a reference to Identity's property, // and that array is deleted when serializing (as it's not seralized), which ends in OOS. this.classes = clone(cmpIdentity.GetClassesList()); }; IdentityMirage.prototype.GetClassesList = function() { return this.classes; }; Engine.RegisterGlobal("IdentityMirage", IdentityMirage); Identity.prototype.Mirage = function() { let mirage = new IdentityMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Identity, "Identity", Identity); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 26298) @@ -1,969 +1,940 @@ function Player() {} Player.prototype.Schema = "" + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI. */ Player.prototype.Serialize = function() { let state = {}; for (let key in this) if (this.hasOwnProperty(key)) state[key] = this[key]; state.diplomacyColor = undefined; state.displayDiplomacyColor = false; return state; }; Player.prototype.Deserialize = function(state) { for (let prop in state) this[prop] = state[prop]; }; /** * Which units will be shown with special icons at the top. */ var panelEntityClasses = "Hero Relic"; Player.prototype.Init = function() { this.playerID = undefined; - this.name = undefined; // Define defaults elsewhere (supporting other languages). - this.civ = undefined; this.color = undefined; this.diplomacyColor = undefined; this.displayDiplomacyColor = false; this.popUsed = 0; // Population of units owned or trained by this player. this.popBonuses = 0; // Sum of population bonuses of player's entities. this.maxPop = 300; // Maximum population. this.trainingBlocked = false; // Indicates whether any training queue is currently blocked. this.resourceCount = {}; this.resourceGatherers = {}; this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100. this.team = -1; // Team number of the player, players on the same team will always have ally diplomatic status. Also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // Game state. One of "active", "defeated", "won". this.diplomacy = []; // Array of diplomatic stances for this player with respect to other players (including gaia and self). this.sharedDropsites = false; this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.cheatsEnabled = false; this.panelEntities = []; this.resourceNames = {}; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.spyCostMultiplier = +this.template.SpyCostMultiplier; this.barterEntities = []; this.barterMultiplier = { "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; // Initial resources. let resCodes = Resources.GetCodes(); for (let res of resCodes) { this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; this.resourceGatherers[res] = 0; } // Trading goods probability in steps of 5. let resTradeCodes = Resources.GetTradableCodes(); let quotient = Math.floor(20 / resTradeCodes.length); let remainder = 20 % resTradeCodes.length; for (let i in resTradeCodes) this.tradingGoods.push({ "goods": resTradeCodes[i], "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; -Player.prototype.SetName = function(name) -{ - this.name = name; -}; - -Player.prototype.GetName = function() -{ - return this.name; -}; - -Player.prototype.SetCiv = function(civcode) -{ - let oldCiv = this.civ; - this.civ = civcode; - // Normally, the civ is only set once. But in Atlas, map designers can change civs at any time. - if (oldCiv && this.playerID && oldCiv != civcode) - Engine.BroadcastMessage(MT_CivChanged, { - "player": this.playerID, - "from": oldCiv, - "to": civcode - }); -}; - -Player.prototype.GetCiv = function() -{ - return this.civ; -}; - Player.prototype.SetColor = function(r, g, b) { let colorInitialized = !!this.color; this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 }; // Used in Atlas. if (colorInitialized) Engine.BroadcastMessage(MT_PlayerColorChanged, { "player": this.playerID }); }; Player.prototype.SetDiplomacyColor = function(color) { this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; }; Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor) { this.displayDiplomacyColor = displayDiplomacyColor; }; Player.prototype.GetColor = function() { return this.color; }; Player.prototype.GetDisplayedColor = function() { return this.displayDiplomacyColor ? this.diplomacyColor : this.color; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.popUsed)) return num - (this.GetPopulationLimit() - this.popUsed); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.AddPopulation = function(num) { this.popUsed += num; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.AddPopulationBonuses = function(num) { this.popBonuses += num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.CanBarter = function() { return this.barterEntities.length > 0; }; Player.prototype.GetBarterMultiplier = function() { return this.barterMultiplier; }; Player.prototype.GetSpyCostMultiplier = function() { return this.spyCostMultiplier; }; Player.prototype.GetPanelEntities = function() { return this.panelEntities; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { for (let res in resources) this.resourceCount[res] = resources[res]; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; Player.prototype.GetResourceGatherers = function() { return this.resourceGatherers; }; /** * @param {string} type - The generic type of resource to add the gatherer for. */ Player.prototype.AddResourceGatherer = function(type) { ++this.resourceGatherers[type]; }; /** * @param {string} type - The generic type of resource to remove the gatherer from. */ Player.prototype.RemoveResourceGatherer = function(type) { --this.resourceGatherers[type]; }; /** * Add resource of specified type to player. * @param {string} type - Generic type of resource. * @param {number} amount - Amount of resource, which should be added. */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += +amount; }; /** * Add resources to player. */ Player.prototype.AddResources = function(amounts) { for (let type in amounts) this.resourceCount[type] += +amounts[type]; }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all. let amountsNeeded = {}; for (let type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]); if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { let amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player. if (amountsNeeded) { let parameters = {}; let i = 0; for (let type in amountsNeeded) { ++i; parameters["resourceType" + i] = this.resourceNames[type]; parameters["resourceAmount" + i] = amountsNeeded[type]; } let msg = ""; // When marking strings for translations, you need to include the actual string, // not some way to derive the string. if (i < 1) warn("Amounts needed but no amounts given?"); else if (i == 1) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s"); else if (i == 2) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s"); else if (i == 3) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s"); else if (i == 4) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s"); else warn("Localisation: Strings are not localised for more than 4 resources"); // Send as time-notification. let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence" } }); return false; } for (let type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (let type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.RefundResources = function(amounts) { const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (const type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]); this.AddResources(amounts); }; Player.prototype.GetNextTradingGoods = function() { let value = randFloat(0, 100); let last = this.tradingGoods.length - 1; let sumProba = 0; for (let i = 0; i < last; ++i) { sumProba += this.tradingGoods[i].proba; if (value < sumProba) return this.tradingGoods[i].goods; } return this.tradingGoods[last].goods; }; Player.prototype.GetTradingGoods = function() { let tradingGoods = {}; for (let resource of this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { let resTradeCodes = Resources.GetTradableCodes(); let sumProba = 0; for (let resource in tradingGoods) { if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } sumProba += tradingGoods[resource]; } if (sumProba != 100) { error("Invalid trading goods probability: " + uneval(sumProba)); return; } this.tradingGoods = []; for (let resource in tradingGoods) this.tradingGoods.push({ "goods": resource, "proba": tradingGoods[resource] }); }; Player.prototype.GetState = function() { return this.state; }; /** * @param {string} newState - Either "defeated" or "won". * @param {string|undefined} message - A string to be shown in chat, for example * markForTranslation("%(player)s has been defeated (failed objective)."). * If it is undefined, the caller MUST send that GUI notification manually. */ Player.prototype.SetState = function(newState, message) { if (this.state != "active") return; if (newState != "won" && newState != "defeated") { warn("Can't change playerstate to " + this.state); return; } if (!this.playerID) { warn("Gaia can't change state."); return; } this.state = newState; let won = newState == "won"; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (won) cmpRangeManager.SetLosRevealAll(this.playerID, true); else { // Reassign all player's entities to Gaia. let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for (let entity of entities) { let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for (let entity of entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); } Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); if (message) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": won ? "won" : "defeat", "players": [this.playerID], "allies": [this.playerID], "message": message }); } }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (this.teamsLocked) return; this.team = team; // Set all team members as allies. if (this.team != -1) { let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); if (this.team != cmpPlayer.GetTeam()) continue; this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy.slice(); }; Player.prototype.SetDiplomacy = function(dipl) { this.diplomacy = dipl.slice(); Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { let cmpPlayer = QueryPlayerIDInterface(idx); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; this.diplomacy[idx] = value; Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": cmpPlayer.GetPlayerID() }); // Mutual worsening of relations. if (cmpPlayer.diplomacy[this.playerID] > value) cmpPlayer.SetDiplomacyIndex(this.playerID, value); }; Player.prototype.UpdateSharedLos = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (!cmpRangeManager || !cmpTechnologyManager) return; if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech)) { cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]); return; } cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies()); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = { "position": pos, "rotation": rot }; }; Player.prototype.HasStartingCamera = function() { return this.startCam !== undefined; }; Player.prototype.HasSharedLos = function() { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); }; Player.prototype.HasSharedDropsites = function() { return this.sharedDropsites; }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.GetPlayersByDiplomacy = function(func) { let players = []; for (let i = 0; i < this.diplomacy.length; ++i) if (this[func](i)) players.push(i); return players; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally. */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; Player.prototype.GetAllies = function() { return this.GetPlayersByDiplomacy("IsAlly"); }; /** * Check if given player is our ally excluding ourself */ Player.prototype.IsExclusiveAlly = function(id) { return this.playerID != id && this.IsAlly(id); }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { let cmpPlayer = QueryPlayerIDInterface(id); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.GetMutualAllies = function() { return this.GetPlayersByDiplomacy("IsMutualAlly"); }; /** * Check if given player is our ally, and we are its ally, excluding ourself */ Player.prototype.IsExclusiveMutualAlly = function(id) { return this.playerID != id && this.IsMutualAlly(id); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.GetEnemies = function() { return this.GetPlayersByDiplomacy("IsEnemy"); }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Do some map dependant initializations */ Player.prototype.OnGlobalInitGame = function(msg) { // Replace the "{civ}" code with this civ ID. let disabledTemplates = this.disabledTemplates; this.disabledTemplates = {}; + const civ = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); for (let template in disabledTemplates) if (disabledTemplates[template]) - this.disabledTemplates[template.replace(/\{civ\}/g, this.civ)] = true; + this.disabledTemplates[template.replace(/\{civ\}/g, civ)] = true; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player. */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); let panelIndex = this.panelEntities.indexOf(msg.entity); if (panelIndex >= 0) this.panelEntities.splice(panelIndex, 1); let barterIndex = this.barterEntities.indexOf(msg.entity); if (barterIndex >= 0) this.barterEntities.splice(barterIndex, 1); } if (msg.to == this.playerID) { if (cmpCost) this.popUsed += cmpCost.GetPopCost(); let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) this.panelEntities.push(msg.entity); if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation)) this.barterEntities.push(msg.entity); } }; Player.prototype.OnResearchFinished = function(msg) { if (msg.tech == this.template.SharedLosTech) this.UpdateSharedLos(); else if (msg.tech == this.template.SharedDropsitesTech) this.sharedDropsites = true; }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.OnValueModification = function(msg) { if (msg.component != "Player") return; if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1) this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity); if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/"))) for (let res in this.template.BarterMultiplier.Buy) { this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity); this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity); } }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.TributeResource = function(player, amounts) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; let resTribCodes = Resources.GetTributableCodes(); for (let resCode in amounts) if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts)); return; } if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0); let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts }); Engine.BroadcastMessage(MT_TributeExchanged, { "to": player, "from": this.playerID, "amounts": amounts }); }; Player.prototype.AddDisabledTemplate = function(template) { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTemplates = function(templates) { this.disabledTemplates = {}; for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTemplates = function() { return this.disabledTemplates; }; Player.prototype.AddDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = false; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTechnologies = function(techs) { this.disabledTechnologies = {}; for (let tech of techs) this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTechnologies = function() { return this.disabledTechnologies; }; Player.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : this.IsAlly(msg.playerId) ? "defeated_ally" : this.state === "won" ? "won" : "defeated_enemy"); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID); }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (revision 26298) @@ -1,373 +1,381 @@ function Researcher() {} Researcher.prototype.Schema = "Allows the entity to research technologies." + "" + "" + "0.5" + "0.1" + "0" + "2" + "" + "" + "" + "\n phase_town_{civ}\n phase_metropolis_ptol\n unlock_shared_los\n wonder_population_cap\n " + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + "" + ""; /** * This object represents a technology being researched. * @param {string} templateName - The name of the template we ought to research. * @param {number} researcher - The entity ID of our researcher. * @param {string} metadata - Optionally any metadata to attach to us. */ Researcher.prototype.Item = function(templateName, researcher, metadata) { this.templateName = templateName; this.researcher = researcher; this.metadata = metadata; }; /** * Prepare for the queue. * @param {Object} techCostMultiplier - The multipliers to use when calculating costs. * @return {boolean} - Whether the item was successfully initiated. */ Researcher.prototype.Item.prototype.Queue = function(techCostMultiplier) { this.player = QueryOwnerInterface(this.researcher).GetPlayerID(); const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); if (!cmpTechnologyManager.QueuedResearch(this.templateName, this.researcher, techCostMultiplier)) return false; return true; }; Researcher.prototype.Item.prototype.Stop = function() { QueryPlayerIDInterface(this.player, IID_TechnologyManager).StoppedResearch(this.templateName); delete this.started; }; /** * Called when the first work is performed. */ Researcher.prototype.Item.prototype.Start = function() { this.started = true; }; Researcher.prototype.Item.prototype.Finish = function() { this.finished = true; }; /** * @param {number} allocatedTime - The time allocated to this item. * @return {number} - The time used for this item. */ Researcher.prototype.Item.prototype.Progress = function(allocatedTime) { if (!this.started) this.Start(); if (this.paused) this.Unpause(); const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); const usedTime = cmpTechnologyManager.Progress(this.templateName, allocatedTime); if (!cmpTechnologyManager.IsTechnologyQueued(this.templateName)) this.Finish(); return usedTime; }; Researcher.prototype.Item.prototype.Pause = function() { QueryPlayerIDInterface(this.player, IID_TechnologyManager).Pause(this.templateName); this.paused = true; }; Researcher.prototype.Item.prototype.Unpause = function() { delete this.paused; }; /** * @return {Object} - Some basic information of this item. */ Researcher.prototype.Item.prototype.GetBasicInfo = function() { const result = QueryPlayerIDInterface(this.player, IID_TechnologyManager).GetBasicInfo(this.templateName); result.technologyTemplate = this.templateName; result.metadata = this.metadata; return result; }; Researcher.prototype.Item.prototype.SerializableAttributes = [ "metadata", "paused", "player", "researcher", "started", "templateName" ]; Researcher.prototype.Item.prototype.Serialize = function(id) { const result = { "id": id }; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; Researcher.prototype.Item.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; }; Researcher.prototype.Init = function() { this.nextID = 1; this.queue = new Map(); }; Researcher.prototype.Serialize = function() { const queue = []; for (const [id, item] of this.queue) queue.push(item.Serialize(id)); return { "nextID": this.nextID, "queue": queue }; }; Researcher.prototype.Deserialize = function(data) { this.Init(); this.nextID = data.nextID; for (const item of data.queue) { const newItem = new this.Item(); newItem.Deserialize(item); this.queue.set(item.id, newItem); } }; /* * Returns list of technologies that can be researched by this entity. */ Researcher.prototype.GetTechnologiesList = function() { const string = ApplyValueModificationsToEntity("Researcher/Technologies/_string", this.template?.Technologies?._string || "", this.entity); if (!string) return []; - const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); + const owner = Engine.QueryInterface(this.entity, IID_Ownership)?.GetOwner(); + if (!owner || owner === INVALID_PLAYER) + return []; + + const playerEnt = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(owner); + if (!playerEnt) + return []; + + const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (!cmpTechnologyManager) return []; - const cmpPlayer = QueryOwnerInterface(this.entity); + const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); if (!cmpPlayer) return []; - const civ = cmpPlayer.GetCiv(); let techs = string.split(/\s+/); // Replace the civ specific technologies. + const civ = Engine.QueryInterface(playerEnt, IID_Identity).GetCiv(); for (let i = 0; i < techs.length; ++i) { const tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; const civTech = tech.replace("{civ}", civ); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } // Remove any technologies that can't be researched by this civ. techs = techs.filter(tech => cmpTechnologyManager.CheckTechnologyRequirements( DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), civ), true)); const techList = []; const superseded = {}; const disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); // Add any top level technologies to an array which corresponds to the displayed icons. // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }. for (const tech of techs) { if (disabledTechnologies && disabledTechnologies[tech]) continue; const template = TechnologyTemplates.Get(tech); if (!template.supersedes || techs.indexOf(template.supersedes) === -1) techList.push(tech); else superseded[template.supersedes] = tech; } // Now make researched/in progress techs invisible. for (const i in techList) { let tech = techList[i]; while (this.IsTechnologyResearchedOrInProgress(tech)) tech = superseded[tech]; techList[i] = tech; } const ret = []; // This inserts the techs into the correct positions to line up the technology pairs. for (let i = 0; i < techList.length; ++i) { const tech = techList[i]; if (!tech) { ret[i] = undefined; continue; } const template = TechnologyTemplates.Get(tech); if (template.top) ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; else ret[i] = tech; } return ret; }; /** * @return {Object} - The multipliers to change the costs of any research with. */ Researcher.prototype.GetTechCostMultiplier = function() { const techCostMultiplier = {}; for (const res of Resources.GetCodes().concat(["time"])) techCostMultiplier[res] = ApplyValueModificationsToEntity( "Researcher/TechCostMultiplier/" + res, +(this.template?.TechCostMultiplier?.[res] || 1), this.entity); return techCostMultiplier; }; /** * Checks whether we can research the given technology, minding paired techs. */ Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return false; const template = TechnologyTemplates.Get(tech); if (template.top) return cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom); return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); }; /** * @param {string} templateName - The technology to queue. * @param {string} metadata - Any metadata attached to the item. * @return {number} - The ID of the item. -1 if the item could not be researched. */ Researcher.prototype.QueueTechnology = function(templateName, metadata) { if (!this.GetTechnologiesList().some(tech => tech && (tech == templateName || tech.pair && (tech.top == templateName || tech.bottom == templateName)))) { error("This entity cannot research " + templateName + "."); return -1; } const item = new this.Item(templateName, this.entity, metadata); const techCostMultiplier = this.GetTechCostMultiplier(); if (!item.Queue(techCostMultiplier)) return -1; const id = this.nextID++; this.queue.set(id, item); return id; }; /** * @param {number} id - The id of the technology researched here we need to stop. */ Researcher.prototype.StopResearching = function(id) { this.queue.get(id).Stop(); this.queue.delete(id); }; /** * @param {number} id - The id of the technology. */ Researcher.prototype.PauseTechnology = function(id) { this.queue.get(id).Pause(); }; /** * @param {number} id - The ID of the item to check. * @return {boolean} - Whether we are currently training the item. */ Researcher.prototype.HasItem = function(id) { return this.queue.has(id); }; /** * @parameter {number} id - The id of the research. * @return {Object} - Some basic information about the research. */ Researcher.prototype.GetResearchingTechnology = function(id) { return this.queue.get(id).GetBasicInfo(); }; /** * @param {number} id - The ID of the item we spent time on. * @param {number} allocatedTime - The time we spent on the given item. * @return {number} - The time we've actually used. */ Researcher.prototype.Progress = function(id, allocatedTime) { const item = this.queue.get(id); const usedTime = item.Progress(allocatedTime); if (item.finished) this.queue.delete(id); return usedTime; }; Engine.RegisterComponentType(IID_Researcher, "Researcher", Researcher); Index: ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js (revision 26298) @@ -1,101 +1,100 @@ function SkirmishReplacer() {} SkirmishReplacer.prototype.Schema = "" + "" + "" + "" + "" + "" + ""; SkirmishReplacer.prototype.Init = function() { }; SkirmishReplacer.prototype.Serialize = null; // We have no dynamic state to save function getReplacementEntities(civ) { return Engine.ReadJSONFile("simulation/data/civs/" + civ + ".json").SkirmishReplacements; } SkirmishReplacer.prototype.OnOwnershipChanged = function(msg) { if (msg.to == 0) warn("Skirmish map elements can only be owned by regular players. Please delete entity "+this.entity+" or change the ownership to a non-gaia player."); }; SkirmishReplacer.prototype.ReplaceEntities = function() { - var cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) + const civ = QueryOwnerInterface(this.entity, IID_Identity)?.GetCiv(); + if (!civ) return; - var civ = cmpPlayer.GetCiv(); var replacementEntities = getReplacementEntities(civ); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let specialFilters = templateName.substr(0, templateName.lastIndexOf("|") + 1); templateName = removeFiltersFromTemplateName(templateName); if (templateName in replacementEntities) templateName = replacementEntities[templateName]; else if (this.template && "general" in this.template) templateName = this.template.general; else templateName = ""; if (!templateName || civ == "gaia") { Engine.DestroyEntity(this.entity); return; } templateName = specialFilters + templateName.replace(/\{civ\}/g, civ); var cmpCurPosition = Engine.QueryInterface(this.entity, IID_Position); var replacement = Engine.AddEntity(templateName); if (!replacement) { Engine.DestroyEntity(this.entity); return; } var cmpReplacementPosition = Engine.QueryInterface(replacement, IID_Position); var pos = cmpCurPosition.GetPosition2D(); cmpReplacementPosition.JumpTo(pos.x, pos.y); var rot = cmpCurPosition.GetRotation(); cmpReplacementPosition.SetYRotation(rot.y); var cmpCurOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpReplacementOwnership = Engine.QueryInterface(replacement, IID_Ownership); cmpReplacementOwnership.SetOwner(cmpCurOwnership.GetOwner()); let msg = { "entity": this.entity, "newentity": replacement }; Engine.PostMessage(this.entity, MT_EntityRenamed, msg); Engine.PostMessage(this.entity, MT_SkirmishReplacerReplaced, msg); Engine.DestroyEntity(this.entity); }; /** * Replace this entity with a civ-specific entity * Message is sent right before InitGame() is called, in InitGame.js * Replacement needs to happen early on real games to not confuse the AI */ SkirmishReplacer.prototype.OnSkirmishReplace = function(msg) { this.ReplaceEntities(); }; /** * Replace this entity with a civ-specific entity * This is needed for Atlas, when the entity isn't replaced before the game starts, * so it needs to be replaced on the first turn. */ SkirmishReplacer.prototype.OnUpdate = function(msg) { this.ReplaceEntities(); }; Engine.RegisterComponentType(IID_SkirmishReplacer, "SkirmishReplacer", SkirmishReplacer); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 26298) @@ -1,552 +1,552 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; /** * This object represents a technology under research. * @param {string} templateName - The name of the template to research. * @param {number} player - The player ID researching. * @param {number} researcher - The entity ID researching. */ TechnologyManager.prototype.Technology = function(templateName, player, researcher) { this.player = player; this.researcher = researcher; this.templateName = templateName; }; /** * Prepare for the queue. * @param {Object} techCostMultiplier - The multipliers to use when calculating costs. * @return {boolean} - Whether the technology was successfully initiated. */ TechnologyManager.prototype.Technology.prototype.Queue = function(techCostMultiplier) { const template = TechnologyTemplates.Get(this.templateName); if (!template) return false; this.resources = {}; if (template.cost) for (const res in template.cost) this.resources[res] = Math.floor(techCostMultiplier[res] * template.cost[res]); // ToDo: Subtract resources here or in cmpResearcher? const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); // TrySubtractResources should report error to player (they ran out of resources). if (!cmpPlayer?.TrySubtractResources(this.resources)) return false; const time = techCostMultiplier.time * (template.researchTime || 0) * 1000; this.timeRemaining = time; this.timeTotal = time; const playerID = cmpPlayer.GetPlayerID(); Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", { "playerid": playerID, "technologyTemplate": this.templateName, "researcherEntity": this.researcher }); return true; }; TechnologyManager.prototype.Technology.prototype.Stop = function() { const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); cmpPlayer?.RefundResources(this.resources); delete this.resources; if (this.started && this.templateName.startsWith("phase")) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": this.templateName, "phaseState": "aborted" }); }; /** * Called when the first work is performed. */ TechnologyManager.prototype.Technology.prototype.Start = function() { this.started = true; if (!this.templateName.startsWith("phase")) return; const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": this.templateName, "phaseState": "started" }); }; TechnologyManager.prototype.Technology.prototype.Finish = function() { this.finished = true; const template = TechnologyTemplates.Get(this.templateName); if (template.soundComplete) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher); if (template.modifications) { const cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("tech/" + this.templateName, DeriveModificationsFromTech(template), this.player); } const cmpEntityLimits = Engine.QueryInterface(this.player, IID_EntityLimits); const cmpTechnologyManager = Engine.QueryInterface(this.player, IID_TechnologyManager); if (template.replaces && template.replaces.length > 0) for (const i of template.replaces) { cmpTechnologyManager.MarkTechnologyAsResearched(i); cmpEntityLimits?.UpdateLimitsFromTech(i); } cmpTechnologyManager.MarkTechnologyAsResearched(this.templateName); // ToDo: Move to EntityLimits.js. cmpEntityLimits?.UpdateLimitsFromTech(this.templateName); const playerID = Engine.QueryInterface(this.player, IID_Player).GetPlayerID(); Engine.PostMessage(this.player, MT_ResearchFinished, { "player": playerID, "tech": this.templateName }); if (this.templateName.startsWith("phase") && !template.autoResearch) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [playerID], "phaseName": this.templateName, "phaseState": "completed" }); }; /** * @param {number} allocatedTime - The time allocated to this item. * @return {number} - The time used for this item. */ TechnologyManager.prototype.Technology.prototype.Progress = function(allocatedTime) { if (!this.started) this.Start(); if (this.paused) this.Unpause(); if (this.timeRemaining > allocatedTime) { this.timeRemaining -= allocatedTime; return allocatedTime; } this.Finish(); return this.timeRemaining; }; TechnologyManager.prototype.Technology.prototype.Pause = function() { this.paused = true; }; TechnologyManager.prototype.Technology.prototype.Unpause = function() { delete this.paused; }; TechnologyManager.prototype.Technology.prototype.GetBasicInfo = function() { return { "paused": this.paused, "progress": 1 - (this.timeRemaining / (this.timeTotal || 1)), "researcher": this.researcher, "templateName": this.templateName, "timeRemaining": this.timeRemaining }; }; TechnologyManager.prototype.Technology.prototype.SerializableAttributes = [ "paused", "player", "researcher", "resources", "started", "templateName", "timeRemaining", "timeTotal" ]; TechnologyManager.prototype.Technology.prototype.Serialize = function() { const result = {}; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; TechnologyManager.prototype.Technology.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; }; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the technology object. this.researchQueued = new Map(); this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.SerializableAttributes = [ "researchedTechs", "classCounts", "typeCountsByClass", "unresearchedAutoResearchTechs" ]; TechnologyManager.prototype.Serialize = function() { const result = {}; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; result.researchQueued = []; for (const [techName, techObject] of this.researchQueued) result.researchQueued.push(techObject.Serialize()); return result; }; TechnologyManager.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; this.researchQueued = new Map(); for (const tech of data.researchQueued) { const newTech = new this.Technology(); newTech.Deserialize(tech); this.researchQueued.set(tech.templateName, newTech); } }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function(templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; - return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); + return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Identity).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } } }; /** * This does neither apply effects nor verify requirements. * @param {string} tech - The name of the technology to mark as researched. */ TechnologyManager.prototype.MarkTechnologyAsResearched = function(tech) { this.researchedTechs.add(tech); this.UpdateAutoResearch(); }; /** * Note that this does not verify whether the requirements are met. * @param {string} tech - The technology to research. * @param {number} researcher - Optionally the entity to couple with the research. */ TechnologyManager.prototype.ResearchTechnology = function(tech, researcher = INVALID_ENTITY) { if (this.IsTechnologyQueued(tech) || this.IsTechnologyResearched(tech)) return; const technology = new this.Technology(tech, this.entity, researcher); technology.Finish(); }; /** * Marks a technology as being queued for research at the given entityID. * @param {string} tech - The technology to queue. * @param {number} researcher - The entity ID of the entity researching this technology. * @param {Object} techCostMultiplier - The multipliers used when calculating the costs. * * @return {boolean} - Whether we successfully have queued the technology. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher, techCostMultiplier) { // ToDo: Check whether the technology is researched already? const technology = new this.Technology(tech, this.entity, researcher); if (!technology.Queue(techCostMultiplier)) return false; this.researchQueued.set(tech, technology); return true; }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. * @param {string} tech - The name of the technology to stop. * @param {boolean} notification - Whether a GUI notification ought to be sent. */ TechnologyManager.prototype.StoppedResearch = function(tech) { this.researchQueued.get(tech).Stop(); this.researchQueued.delete(tech); }; /** * @param {string} tech - */ TechnologyManager.prototype.Pause = function(tech) { this.researchQueued.get(tech).Pause(); }; /** * @param {string} tech - The technology to advance. * @param {number} allocatedTime - The time allocated to the technology. * @return {number} - The time we've actually used. */ TechnologyManager.prototype.Progress = function(techName, allocatedTime) { const technology = this.researchQueued.get(techName); const usedTime = technology.Progress(allocatedTime); if (technology.finished) this.researchQueued.delete(techName); return usedTime; }; /** * @param {string} tech - The technology name to retreive some basic information for. * @return {Object} - Some basic information about the technology under research. */ TechnologyManager.prototype.GetBasicInfo = function(tech) { return this.researchQueued.get(tech).GetBasicInfo(); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.GetBasicInfoOfStartedTechs = function() { const result = {}; for (const [techName, tech] of this.researchQueued) if (tech.started) result[techName] = tech.GetBasicInfo(); return result; }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26298) @@ -1,727 +1,727 @@ function Trainer() {} Trainer.prototype.Schema = "Allows the entity to train new units." + "" + "0.7" + "" + "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + ""; /** * This object represents a batch of entities being trained. * @param {string} templateName - The name of the template we ought to train. * @param {number} count - The size of the batch to train. * @param {number} trainer - The entity ID of our trainer. * @param {string} metadata - Optionally any metadata to attach to us. */ Trainer.prototype.Item = function(templateName, count, trainer, metadata) { this.count = count; this.templateName = templateName; this.trainer = trainer; this.metadata = metadata; }; /** * Prepare for the queue. * @param {Object} trainCostMultiplier - The multipliers to use when calculating costs. * @param {number} batchTimeMultiplier - The factor to use when training this batches. * * @return {boolean} - Whether the item was successfully initiated. */ Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier) { if (!Number.isInteger(this.count) || this.count <= 0) { error("Invalid batch count " + this.count + "."); return false; } const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); const template = cmpTemplateManager.GetTemplate(this.templateName); if (!template) return false; const cmpPlayer = QueryOwnerInterface(this.trainer); if (!cmpPlayer) return false; this.player = cmpPlayer.GetPlayerID(); this.resources = {}; const totalResources = {}; for (const res in template.Cost.Resources) { this.resources[res] = trainCostMultiplier[res] * ApplyValueModificationsToTemplate( "Cost/Resources/" + res, +template.Cost.Resources[res], this.player, template); totalResources[res] = Math.floor(this.count * this.resources[res]); } // TrySubtractResources should report error to player (they ran out of resources). if (!cmpPlayer.TrySubtractResources(totalResources)) return false; this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template); if (template.TrainingRestrictions) { const unitCategory = template.TrainingRestrictions.Category; const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); if (cmpPlayerEntityLimits) { if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit)) // Already warned, return. { cmpPlayer.RefundResources(totalResources); return false; } // ToDo: Should warn here v and return? cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count); if (template.TrainingRestrictions.MatchLimit) cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count); } } const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template); const time = batchTimeMultiplier * trainCostMultiplier.time * buildTime * 1000; this.timeRemaining = time; this.timeTotal = time; const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("OnTrainingQueued", { "playerid": this.player, "unitTemplate": this.templateName, "count": this.count, "metadata": this.metadata, "trainerEntity": this.trainer }); return true; }; /** * Destroy cached entities, refund resources and free (population) limits. */ Trainer.prototype.Item.prototype.Stop = function() { // Destroy any cached entities (those which didn't spawn for some reason). if (this.entities?.length) { for (const ent of this.entities) Engine.DestroyEntity(ent); delete this.entities; } const cmpPlayer = QueryPlayerIDInterface(this.player); const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); const template = cmpTemplateManager.GetTemplate(this.templateName); if (template.TrainingRestrictions) { const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count); if (template.TrainingRestrictions.MatchLimit) cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count); } const cmpStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker); const totalCosts = {}; for (const resource in this.resources) { totalCosts[resource] = Math.floor(this.count * this.resources[resource]); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]); } if (cmpPlayer) { if (this.started) cmpPlayer.UnReservePopulationSlots(this.population * this.count); cmpPlayer.RefundResources(totalCosts); cmpPlayer.UnBlockTraining(); } delete this.resources; }; /** * This starts the item, reserving population. * @return {boolean} - Whether the item was started successfully. */ Trainer.prototype.Item.prototype.Start = function() { const cmpPlayer = QueryPlayerIDInterface(this.player); if (!cmpPlayer) return false; const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName); this.population = ApplyValueModificationsToTemplate( "Cost/Population", +template.Cost.Population, this.player, template); this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count); if (this.missingPopSpace) { cmpPlayer.BlockTraining(); return false; } cmpPlayer.UnBlockTraining(); Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer }); this.started = true; return true; }; Trainer.prototype.Item.prototype.Finish = function() { this.Spawn(); if (!this.count) this.finished = true; }; /* * This function creates the entities and places them in world if possible * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). */ Trainer.prototype.Item.prototype.Spawn = function() { const createdEnts = []; const spawnedEnts = []; // We need entities to test spawning, but we don't want to waste resources, // so only create them once and use as needed. if (!this.entities) { this.entities = []; for (let i = 0; i < this.count; ++i) this.entities.push(Engine.AddEntity(this.templateName)); } let autoGarrison; const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint); if (cmpRallyPoint) { const data = cmpRallyPoint.GetData()[0]; if (data?.target && data.target == this.trainer && data.command == "garrison") autoGarrison = true; } const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint); const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position); const positionTrainer = cmpPosition && cmpPosition.GetPosition(); const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker); while (this.entities.length) { const ent = this.entities[0]; const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); let garrisoned = false; if (autoGarrison) { const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) { // Temporary owner affectation needed for GarrisonHolder checks. cmpNewOwnership.SetOwnerQuiet(this.player); garrisoned = cmpGarrisonable.Garrison(this.trainer); cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); } } if (!garrisoned) { const pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) break; const cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); if (positionTrainer) cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos)); spawnedEnts.push(ent); } // Decrement entity count in the EntityLimits component // since it will be increased by EntityLimits.OnGlobalOwnershipChanged, // i.e. we replace a 'trained' entity by 'alive' one. // Must be done after spawn check so EntityLimits decrements only if unit spawns. if (cmpPlayerEntityLimits) { const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); if (cmpTrainingRestrictions) cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1); } cmpNewOwnership.SetOwner(this.player); if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); this.count--; this.entities.shift(); createdEnts.push(ent); } if (spawnedEnts.length && !autoGarrison && cmpRallyPoint) for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts)) ProcessCommand(this.player, com); const cmpPlayer = QueryOwnerInterface(this.trainer); if (createdEnts.length) { if (this.population) cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length); // Play a sound, but only for the first in the batch (to avoid nasty phasing effects). PlaySound("trained", createdEnts[0]); Engine.PostMessage(this.trainer, MT_TrainingFinished, { "entities": createdEnts, "owner": this.player, "metadata": this.metadata }); } if (this.count) { cmpPlayer.BlockTraining(); if (!this.spawnNotified) { Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Can't find free space to spawn trained units."), "translateMessage": true }); this.spawnNotified = true; } } else { cmpPlayer.UnBlockTraining(); delete this.spawnNotified; } }; /** * @param {number} allocatedTime - The time allocated to this item. * @return {number} - The time used for this item. */ Trainer.prototype.Item.prototype.Progress = function(allocatedTime) { if (this.paused) this.Unpause(); // We couldn't start this timeout, try again later. if (!this.started && !this.Start()) return allocatedTime; if (this.timeRemaining > allocatedTime) { this.timeRemaining -= allocatedTime; return allocatedTime; } this.Finish(); return this.timeRemaining; }; Trainer.prototype.Item.prototype.Pause = function() { this.paused = true; }; Trainer.prototype.Item.prototype.Unpause = function() { delete this.paused; }; /** * @return {Object} - Some basic information of this batch. */ Trainer.prototype.Item.prototype.GetBasicInfo = function() { return { "unitTemplate": this.templateName, "count": this.count, "neededSlots": this.missingPopSpace, "progress": 1 - (this.timeRemaining / (this.timeTotal || 1)), "timeRemaining": this.timeRemaining, "paused": this.paused, "metadata": this.metadata }; }; Trainer.prototype.Item.prototype.SerializableAttributes = [ "count", "entities", "metadata", "missingPopSpace", "paused", "player", "population", "trainer", "resources", "started", "templateName", "timeRemaining", "timeTotal" ]; Trainer.prototype.Item.prototype.Serialize = function(id) { const result = { "id": id }; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; Trainer.prototype.Item.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; }; Trainer.prototype.Init = function() { this.nextID = 1; this.queue = new Map(); this.trainCostMultiplier = {}; }; Trainer.prototype.SerializableAttributes = [ "entitiesMap", "nextID", "trainCostMultiplier" ]; Trainer.prototype.Serialize = function() { const queue = []; for (const [id, item] of this.queue) queue.push(item.Serialize(id)); const result = { "queue": queue }; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; Trainer.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; this.queue = new Map(); for (const item of data.queue) { const newItem = new this.Item(); newItem.Deserialize(item); this.queue.set(item.id, newItem); } }; /* * Returns list of entities that can be trained by this entity. */ Trainer.prototype.GetEntitiesList = function() { return Array.from(this.entitiesMap.values()); }; /** * Calculate the new list of producible entities * and update any entities currently being produced. */ Trainer.prototype.CalculateEntitiesMap = function() { // Don't reset the map, it's used below to update entities. if (!this.entitiesMap) this.entitiesMap = new Map(); const string = this.template?.Entities?._string || ""; // Tokens can be added -> process an empty list to get them. let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity); if (!addedTokens && !string) return; addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/); const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); const cmpPlayer = QueryOwnerInterface(this.entity); const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {}; /** * Process tokens: * - process token modifiers (this is a bit tricky). * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID * - remove disabled entities * - upgrade templates where necessary * This also updates currently queued production (it's more convenient to do it here). */ const removeAllQueuedTemplate = (token) => { const queue = clone(this.queue); const template = this.entitiesMap.get(token); for (const [id, item] of queue) if (item.templateName == template) this.StopBatch(id); }; // ToDo: Notice this doesn't account for entity limits changing due to the template change. const updateAllQueuedTemplate = (token, updateTo) => { const template = this.entitiesMap.get(token); for (const [id, item] of this.queue) if (item.templateName === template) item.templateName = updateTo; }; const toks = string.split(/\s+/); for (const tok of addedTokens) toks.push(tok); const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv(); - const playerCiv = cmpPlayer?.GetCiv(); + const playerCiv = QueryOwnerInterface(this.entity, IID_Identity)?.GetCiv(); const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {}); this.entitiesMap = toks.reduce((entMap, token) => { const rawToken = token; if (!(token in addedDict)) { // This is a bit wasteful but I can't think of a simpler/better way. // The list of token is unlikely to be a performance bottleneck anyways. token = ApplyValueModificationsToEntity("Trainer/Entities/_string", token, this.entity); token = token.split(/\s+/); if (token.every(tok => addedTokens.indexOf(tok) !== -1)) { removeAllQueuedTemplate(rawToken); return entMap; } token = token[0]; } // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. if (nativeCiv) token = token.replace(/\{native\}/g, nativeCiv); if (playerCiv) token = token.replace(/\{civ\}/g, playerCiv); // Filter out disabled and invalid entities. if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token)) { removeAllQueuedTemplate(rawToken); return entMap; } token = this.GetUpgradedTemplate(token); entMap.set(rawToken, token); updateAllQueuedTemplate(rawToken, token); return entMap; }, new Map()); this.CalculateTrainCostMultiplier(); }; /* * Returns the upgraded template name if necessary. */ Trainer.prototype.GetUpgradedTemplate = function(templateName) { const cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return templateName; const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { const requiredXp = ApplyValueModificationsToTemplate( "Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp > 0) break; templateName = template.Promotion.Entity; template = cmpTemplateManager.GetTemplate(templateName); } return templateName; }; Trainer.prototype.CalculateTrainCostMultiplier = function() { for (const res of Resources.GetCodes().concat(["time"])) this.trainCostMultiplier[res] = ApplyValueModificationsToEntity( "Trainer/TrainCostMultiplier/" + res, +(this.template?.TrainCostMultiplier?.[res] || 1), this.entity); }; /** * @return {Object} - The multipliers to change the costs of any training activity with. */ Trainer.prototype.TrainCostMultiplier = function() { return this.trainCostMultiplier; }; /* * Returns batch build time. */ Trainer.prototype.GetBatchTime = function(batchSize) { // TODO: work out what equation we should use here. return Math.pow(batchSize, ApplyValueModificationsToEntity( "Trainer/BatchTimeModifier", +(this.template?.BatchTimeModifier || 1), this.entity)); }; /** * @param {string} templateName - The template name to check. * @return {boolean} - Whether we can train this template. */ Trainer.prototype.CanTrain = function(templateName) { return this.GetEntitiesList().includes(templateName); }; /** * @param {string} templateName - The entity to queue. * @param {number} count - The batch size. * @param {string} metadata - Any metadata attached to the item. * * @return {number} - The ID of the item. -1 if the item could not be queued. */ Trainer.prototype.QueueBatch = function(templateName, count, metadata) { const item = new this.Item(templateName, count, this.entity, metadata); if (!item.Queue(this.TrainCostMultiplier(), this.GetBatchTime(count))) return -1; const id = this.nextID++; this.queue.set(id, item); return id; }; /** * @param {number} id - The ID of the batch being trained here we need to stop. */ Trainer.prototype.StopBatch = function(id) { this.queue.get(id).Stop(); this.queue.delete(id); }; /** * @param {number} id - The ID of the training. */ Trainer.prototype.PauseBatch = function(id) { this.queue.get(id).Pause(); }; /** * @param {number} id - The ID of the batch to check. * @return {boolean} - Whether we are currently training the batch. */ Trainer.prototype.HasBatch = function(id) { return this.queue.has(id); }; /** * @parameter {number} id - The id of the training. * @return {Object} - Some basic information about the training. */ Trainer.prototype.GetBatch = function(id) { const item = this.queue.get(id); return item?.GetBasicInfo(); }; /** * @param {number} id - The ID of the item we spent time on. * @param {number} allocatedTime - The time we spent on the given item. * @return {number} - The time we've actually used. */ Trainer.prototype.Progress = function(id, allocatedTime) { const item = this.queue.get(id); const usedTime = item.Progress(allocatedTime); if (item.finished) this.queue.delete(id); return usedTime; }; Trainer.prototype.OnCivChanged = function() { this.CalculateEntitiesMap(); }; Trainer.prototype.OnOwnershipChanged = function(msg) { if (msg.to != INVALID_PLAYER) this.CalculateEntitiesMap(); }; Trainer.prototype.OnValueModification = function(msg) { // If the promotion requirements of units is changed, // update the entities list so that automatically promoted units are shown // appropriately in the list. if (msg.component != "Promotion" && (msg.component != "Trainer" || !msg.valueNames.some(val => val.startsWith("Trainer/Entities/")))) return; if (msg.entities.indexOf(this.entity) === -1) return; // This also updates the queued production if necessary. this.CalculateEntitiesMap(); // Inform the GUI that it'll need to recompute the selection panel. // TODO: it would be better to only send the message if something actually changing // for the current training queue. const cmpPlayer = QueryOwnerInterface(this.entity); if (cmpPlayer) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); }; Trainer.prototype.OnDisabledTemplatesChanged = function(msg) { this.CalculateEntitiesMap(); }; Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 26298) @@ -1,190 +1,194 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Builder.js"); Engine.LoadComponentScript("Health.js"); Engine.LoadComponentScript("Repairable.js"); Engine.LoadComponentScript("Timer.js"); const builderId = 6; const target = 7; const playerId = 1; const playerEntityID = 2; AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => true }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true }); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal); function testEntitiesList() { let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" } }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), []); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({}), "GetPlayerID": () => playerId }); + AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", + }); + AddMock(builderId, IID_Ownership, { "GetOwner": () => playerId }); AddMock(builderId, IID_Identity, { "GetCiv": () => "iber" }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": name => name == "structures/iber/civil_centre" }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true }); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), "GetPlayerID": () => playerId }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({ "structures/iber/barracks": true }), "GetPlayerID": () => playerId }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre", "structures/iber/house"]); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), "GetPlayerID": () => playerId }); + AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "athen", + }); + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/athen/civil_centre", "structures/iber/house"]); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 2, "min": 0 }); AddMock(builderId, IID_Obstruction, { "GetSize": () => 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 3, "min": 0 }); } testEntitiesList(); function testBuildingFoundation() { let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "" } }); AddMock(playerEntityID, IID_Player, { "IsAlly": (p) => p == playerId }); AddMock(target, IID_Ownership, { "GetOwner": () => playerId }); let increased = false; AddMock(target, IID_Foundation, { "Build": (entity, amount) => { increased = true; TS_ASSERT_EQUALS(amount, 1); }, "AddBuilder": () => {} }); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(increased); increased = false; cmpTimer.OnUpdate({ "turnLength": 2 }); TS_ASSERT(increased); } testBuildingFoundation(); function testRepairing() { AddMock(playerEntityID, IID_Player, { "IsAlly": (p) => p == playerId }); let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "" } }); AddMock(target, IID_Ownership, { "GetOwner": () => playerId }); AddMock(target, IID_Cost, { "GetBuildTime": () => 100 }); let cmpTargetHealth = ConstructComponent(target, "Health", { "Max": 100, "RegenRate": 0, "IdleRegenRate": 0, "DeathType": "vanish", "Unhealable": false }); cmpTargetHealth.SetHitpoints(50); DeleteMock(target, IID_Foundation); let cmpTargetRepairable = ConstructComponent(target, "Repairable", { "RepairTimeRatio": 1, }); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); TS_ASSERT(cmpTargetRepairable.IsRepairable()); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 51); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); cmpTargetRepairable.SetRepairability(false); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); cmpTargetRepairable.SetRepairability(true); // Check that we indeed stopped - shouldn't restart on its own. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 53); } testRepairing(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 26298) @@ -1,611 +1,617 @@ Engine.LoadHelperScript("ObstructionSnap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Market.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/Researcher.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceTrickle.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Trader.js"); Engine.LoadComponentScript("interfaces/Trainer.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Treasure.js"); Engine.LoadComponentScript("interfaces/TreasureCollector.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("interfaces/Upkeep.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetNames": () => ({ "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" }), "GetResource": resource => ({ "aiAnalysisInfluenceGroup": resource == "food" ? "ignore" : resource == "wood" ? "abundant" : "sparse" }) }; var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { "GetPrices": function() { return { "buy": { "food": 150 }, "sell": { "food": 25 } }; } }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { "GetVictoryConditions": () => ["conquest", "wonder"], "GetAlliedVictory": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": function() { return 2; }, "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }, "GetMaxWorldPopulation": function() {} }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetLosVisibility": function(ent, player) { return "visible"; }, "GetLosCircular": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "example"; }, "GetTemplate": function(name) { return ""; } }); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetTime": function() { return 0; }, "SetTimeout": function(ent, iid, funcname, time, data) { return 0; } }); AddMock(100, IID_Player, { - "GetName": function() { return "Player 1"; }, - "GetCiv": function() { return "gaia"; }, "GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; }, "CanControlAllUnits": function() { return false; }, "GetPopulationCount": function() { return 10; }, "GetPopulationLimit": function() { return 20; }, "GetMaxPopulation": function() { return 200; }, "GetResourceCounts": function() { return { "food": 100 }; }, "GetResourceGatherers": function() { return { "food": 1 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() { return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return false; }, "IsMutualAlly": function() { return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return true; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); +AddMock(100, IID_Identity, { + "GetName": function() { return "Player 1"; }, + "GetCiv": function() { return "gaia"; }, +}); + AddMock(100, IID_EntityLimits, { "GetLimits": function() { return { "Foo": 10 }; }, "GetCounts": function() { return { "Foo": 5 }; }, "GetLimitChangers": function() { return { "Foo": {} }; }, "GetMatchCounts": function() { return { "Bar": 0 }; } }); AddMock(100, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(100, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); AddMock(101, IID_Player, { - "GetName": function() { return "Player 2"; }, - "GetCiv": function() { return "mace"; }, "GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; }, "CanControlAllUnits": function() { return true; }, "GetPopulationCount": function() { return 40; }, "GetPopulationLimit": function() { return 30; }, "GetMaxPopulation": function() { return 300; }, "GetResourceCounts": function() { return { "food": 200 }; }, "GetResourceGatherers": function() { return { "food": 3 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() {return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return true; }, "IsMutualAlly": function() {return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return false; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); +AddMock(101, IID_Identity, { + "GetName": function() { return "Player 2"; }, + "GetCiv": function() { return "mace"; }, +}); + AddMock(101, IID_EntityLimits, { "GetLimits": function() { return { "Bar": 20 }; }, "GetCounts": function() { return { "Bar": 0 }; }, "GetLimitChangers": function() { return { "Bar": {} }; }, "GetMatchCounts": function() { return { "Foo": 0 }; } }); AddMock(101, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(101, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); AddMock(10, IID_Builder, { "GetEntitiesList": function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { "GetHitpoints": function() { return 50; }, "GetMaxHitpoints": function() { return 60; }, "IsRepairable": function() { return false; }, "IsUnhealable": function() { return false; } }); AddMock(10, IID_Identity, { "GetClassesList": function() { return ["class1", "class2"]; }, "GetRank": function() { return "foo"; }, "GetSelectionGroupName": function() { return "Selection Group Name"; }, "HasClass": function() { return true; }, "IsUndeletable": function() { return false; }, "IsControllable": function() { return true; }, }); AddMock(10, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return { "x": 1, "y": 2, "z": 3 }; }, "IsInWorld": function() { return true; } }); AddMock(10, IID_ResourceTrickle, { "GetInterval": () => 1250, "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 }) }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { "id": 10, "player": INVALID_PLAYER, "template": "example", "identity": { "rank": "foo", "classes": ["class1", "class2"], "selectionGroupName": "Selection Group Name", "canDelete": true, "controllable": true, }, "position": { "x": 1, "y": 2, "z": 3 }, "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true, "builder": true, "visibility": "visible", "isBarterMarket": true, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } } }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (revision 26298) @@ -1,152 +1,162 @@ Engine.RegisterGlobal("Resources", { "BuildSchema": (a, b) => {}, "GetCodes": () => ["food"] }); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Researcher.js"); Engine.LoadComponentScript("Researcher.js"); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); const playerID = 1; const playerEntityID = 11; const entityID = 21; Engine.RegisterGlobal("TechnologyTemplates", { "Has": name => name == "phase_town_athen" || name == "phase_city_athen", "Get": () => ({}) }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}) // ToDo: Should be in the techmanager. }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); + AddMock(playerEntityID, IID_TechnologyManager, { "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, "IsTechnologyResearched": () => false }); AddMock(entityID, IID_Ownership, { "GetOwner": () => playerID }); AddMock(entityID, IID_Identity, { "GetCiv": () => "iber" }); let cmpResearcher = ConstructComponent(entityID, "Researcher", { "Technologies": { "_string": "gather_fishing_net " + "phase_town_{civ} " + "phase_city_{civ}" } }); TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] ); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }) }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "athen", +}); TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), ["phase_town_athen", "phase_city_athen"]); AddMock(playerEntityID, IID_TechnologyManager, { "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, "IsTechnologyResearched": tech => tech == "phase_town_athen" }); TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [undefined, "phase_city_athen"]); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}) }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] ); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => typeof value === "string" ? value + " some_test": value); TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), ["gather_fishing_net", "phase_town_generic", "phase_city_generic", "some_test"] ); // Test Queuing a tech. const queuedTech = "gather_fishing_net"; const cost = { "food": 10 }; Engine.RegisterGlobal("TechnologyTemplates", { "Has": () => true, "Get": () => ({ "cost": cost, "researchTime": 1 }) }); const cmpPlayer = AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}), "GetPlayerID": () => playerID, }); + +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); const techManager = AddMock(playerEntityID, IID_TechnologyManager, { "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, "IsTechnologyResearched": () => false, "QueuedResearch": (templateName, researcher, techCostMultiplier) => { TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); TS_ASSERT_UNEVAL_EQUALS(researcher, entityID); return true; }, "StoppedResearch": (templateName, _) => { TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); }, "StartedResearch": (templateName, _) => { TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); }, "ResearchTechnology": (templateName, _) => { TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); } }); let spyTechManager = new Spy(techManager, "QueuedResearch"); let id = cmpResearcher.QueueTechnology(queuedTech); TS_ASSERT_EQUALS(spyTechManager._called, 1); TS_ASSERT_EQUALS(cmpResearcher.queue.size, 1); // Test removing a queued tech. spyTechManager = new Spy(techManager, "StoppedResearch"); cmpResearcher.StopResearching(id); TS_ASSERT_EQUALS(spyTechManager._called, 1); TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0); // Test finishing a queued tech. id = cmpResearcher.QueueTechnology(queuedTech); techManager.Progress = () => 500; techManager.IsTechnologyQueued = () => true; TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 500), 500); cmpResearcher = SerializationCycle(cmpResearcher); techManager.IsTechnologyQueued = () => false; TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 500); TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0); // Test that we can affect an empty researcher. Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value + "some_test"); TS_ASSERT_UNEVAL_EQUALS( ConstructComponent(entityID, "Researcher", null).GetTechnologiesList(), ["some_test"] ); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js (revision 26298) @@ -1,119 +1,121 @@ Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetTradableCodes": () => ["food", "metal", "stone", "wood"], "GetBarterableCodes": () => ["food", "metal", "stone", "wood"], "BuildSchema": () => { let schema = ""; for (let res of ["food", "metal"]) { for (let subtype in ["meat", "grain"]) schema += "" + res + "." + subtype + ""; schema += " treasure." + res + ""; } return "" + schema + ""; }, "BuildChoicesSchema": () => { let schema = ""; for (let res of ["food", "metal"]) { for (let subtype in ["meat", "grain"]) schema += "" + res + "." + subtype + ""; schema += " treasure." + res + ""; } return "" + schema + ""; }, "GetResource": (type) => { return { "subtypes": { "meat": "meat", "grain": "grain" } }; } }; Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Researcher.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("Player.js"); Engine.LoadComponentScript("Researcher.js"); Engine.LoadComponentScript("TechnologyManager.js"); Engine.LoadComponentScript("Timer.js"); Engine.LoadComponentScript("Trigger.js"); Engine.LoadHelperScript("Player.js"); ConstructComponent(SYSTEM_ENTITY, "Trigger"); const cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", null); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => typeof value === "string" ? value + " some_test": value); const template = { "name": "templateName" }; Engine.RegisterGlobal("TechnologyTemplates", { "GetAll": () => [], "Get": (tech) => { return template; } }); const playerID = 1; const playerEntityID = 11; const researcherID = 21; let cmpTechnologyManager = ConstructComponent(playerEntityID, "TechnologyManager", null); AddMock(researcherID, IID_Ownership, { "GetOwner": () => playerID }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); - +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "gaia" +}); template.cost = { "food": 100 }; template.researchTime = 1.5; const cmpPlayer = ConstructComponent(playerEntityID, "Player", { "SpyCostMultiplier": "1", "BarterMultiplier": { "Buy": {}, "Sell": {} } }); const spyPlayerResSub = new Spy(cmpPlayer, "TrySubtractResources"); const spyPlayerResRefund = new Spy(cmpPlayer, "RefundResources"); let cmpResearcher = ConstructComponent(researcherID, "Researcher", { "Technologies": { "_string": template.name } }); let id = cmpResearcher.QueueTechnology(template.name); TS_ASSERT_EQUALS(spyPlayerResSub._called, 1); TS_ASSERT(!cmpTechnologyManager.IsTechnologyResearched(template.name)); TS_ASSERT(cmpTechnologyManager.IsInProgress(template.name)); TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 1000); cmpResearcher = SerializationCycle(cmpResearcher); cmpTechnologyManager = SerializationCycle(cmpTechnologyManager); cmpResearcher.StopResearching(id); TS_ASSERT(!cmpTechnologyManager.IsInProgress(template.name)); TS_ASSERT_EQUALS(spyPlayerResRefund._called, 1); id = cmpResearcher.QueueTechnology(template.name); TS_ASSERT_EQUALS(spyPlayerResSub._called, 2); TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 1000); cmpResearcher = SerializationCycle(cmpResearcher); cmpTechnologyManager = SerializationCycle(cmpTechnologyManager); TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 500); TS_ASSERT(cmpTechnologyManager.IsTechnologyResearched(template.name)); TS_ASSERT_EQUALS(spyPlayerResRefund._called, 1); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (revision 26298) @@ -1,315 +1,321 @@ Engine.RegisterGlobal("Resources", { "BuildSchema": (a, b) => {}, "GetCodes": () => ["food"] }); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/Trainer.js"); Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("EntityLimits.js"); Engine.LoadComponentScript("Trainer.js"); Engine.LoadComponentScript("TrainingRestrictions.js"); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); const playerID = 1; const playerEntityID = 11; const entityID = 21; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true, "GetTemplate": name => ({}) }); let cmpTrainer = ConstructComponent(entityID, "Trainer", { "Entities": { "_string": "units/{civ}/cavalry_javelineer_b " + "units/{civ}/infantry_swordsman_b " + "units/{native}/support_female_citizen" } }); cmpTrainer.GetUpgradedTemplate = (template) => template; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({}), "GetPlayerID": () => playerID }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); + AddMock(entityID, IID_Ownership, { "GetOwner": () => playerID }); AddMock(entityID, IID_Identity, { "GetCiv": () => "iber" }); cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] ); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": name => name == "units/iber/support_female_citizen", "GetTemplate": name => ({}) }); cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS(cmpTrainer.GetEntitiesList(), ["units/iber/support_female_citizen"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true, "GetTemplate": name => ({}) }); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), "GetPlayerID": () => playerID }); cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] ); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }), "GetPlayerID": () => playerID }); cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"] ); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), "GetPlayerID": () => playerID }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "athen", +}); + cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"] ); AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": false }), "GetPlayerID": () => playerID }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); + cmpTrainer.CalculateEntitiesMap(); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] ); // Test Queuing a unit. const queuedUnit = "units/iber/infantry_swordsman_b"; const cost = { "food": 10 }; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true, "GetTemplate": name => ({ "Cost": { "BuildTime": 1, "Population": 1, "Resources": cost }, "TrainingRestrictions": { "Category": "some_limit", "MatchLimit": "7" } }) }); AddMock(SYSTEM_ENTITY, IID_Trigger, { "CallEvent": () => {} }); AddMock(SYSTEM_ENTITY, IID_GuiInterface, { "PushNotification": () => {}, "SetSelectionDirty": () => {} }); const cmpPlayer = AddMock(playerEntityID, IID_Player, { "BlockTraining": () => {}, - "GetCiv": () => "iber", "GetPlayerID": () => playerID, "RefundResources": (resources) => { TS_ASSERT_UNEVAL_EQUALS(resources, cost); }, "TrySubtractResources": (resources) => { TS_ASSERT_UNEVAL_EQUALS(resources, cost); // Just have enough resources. return true; }, "TryReservePopulationSlots": () => false, // Always have pop space. "UnReservePopulationSlots": () => {}, // Always have pop space. "UnBlockTraining": () => {}, "GetDisabledTemplates": () => ({}) }); const spyCmpPlayerSubtract = new Spy(cmpPlayer, "TrySubtractResources"); const spyCmpPlayerRefund = new Spy(cmpPlayer, "RefundResources"); const spyCmpPlayerPop = new Spy(cmpPlayer, "TryReservePopulationSlots"); ConstructComponent(playerEntityID, "EntityLimits", { "Limits": { "some_limit": 0 }, "LimitChangers": {}, "LimitRemovers": {} }); // Test that we can't exceed the entity limit. TS_ASSERT_EQUALS(cmpTrainer.QueueBatch(queuedUnit, 1), -1); // And that in that case, the resources are not lost. // ToDo: This is a bad test, it relies on the order of subtraction in the cmp. // Better would it be to check the states before and after the queue. TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, spyCmpPlayerRefund._called); ConstructComponent(playerEntityID, "EntityLimits", { "Limits": { "some_limit": 5 }, "LimitChangers": {}, "LimitRemovers": {} }); let id = cmpTrainer.QueueBatch(queuedUnit, 1); TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 2); TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1); // Test removing a queued batch. cmpTrainer.StopBatch(id); TS_ASSERT_EQUALS(spyCmpPlayerRefund._called, 2); TS_ASSERT_EQUALS(cmpTrainer.queue.size, 0); const cmpEntLimits = QueryOwnerInterface(entityID, IID_EntityLimits); TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5)); // Test finishing a queued batch. id = cmpTrainer.QueueBatch(queuedUnit, 1); TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4)); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0); TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 500), 500); TS_ASSERT_EQUALS(spyCmpPlayerPop._called, 1); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0.5); const spawedEntityIDs = [4, 5, 6, 7, 8]; let spawned = 0; Engine.AddEntity = () => { const ent = spawedEntityIDs[spawned++]; ConstructComponent(ent, "TrainingRestrictions", { "Category": "some_limit" }); AddMock(ent, IID_Identity, { "GetClassesList": () => [] }); AddMock(ent, IID_Position, { "JumpTo": () => {} }); AddMock(ent, IID_Ownership, { "SetOwner": (pid) => { QueryOwnerInterface(ent, IID_EntityLimits).OnGlobalOwnershipChanged({ "entity": ent, "from": -1, "to": pid }); }, "GetOwner": () => playerID }); return ent; }; AddMock(entityID, IID_Footprint, { "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 }) }); cmpTrainer = SerializationCycle(cmpTrainer); TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 1000), 500); TS_ASSERT(!cmpTrainer.HasBatch(id)); TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 5)); TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4)); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1); TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1); // Now check that it doesn't get updated when the spawn doesn't succeed. (regression_test_d1879) cmpPlayer.TrySubtractResources = () => true; cmpPlayer.RefundResources = () => {}; AddMock(entityID, IID_Footprint, { "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 }) }); id = cmpTrainer.QueueBatch(queuedUnit, 2); TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 2000), 2000); TS_ASSERT(cmpTrainer.HasBatch(id)); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 3); cmpTrainer = SerializationCycle(cmpTrainer); // Check that when the batch is removed the counts are subtracted again. cmpTrainer.StopBatch(id); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1); TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1); const queuedSecondUnit = "units/iber/cavalry_javelineer_b"; // Check changing the allowed entities has effect. const id1 = cmpTrainer.QueueBatch(queuedUnit, 1); const id2 = cmpTrainer.QueueBatch(queuedSecondUnit, 1); TS_ASSERT_EQUALS(cmpTrainer.queue.size, 2); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1).unitTemplate, queuedUnit); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, queuedSecondUnit); // Add a modifier that replaces unit A with unit C, // adds a unit D and removes unit B from the roster. Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => { return typeof val === "string" ? HandleTokens(val, "units/{civ}/cavalry_javelineer_b>units/{civ}/c units/{civ}/d -units/{civ}/infantry_swordsman_b") : val; }); cmpTrainer.OnValueModification({ "component": "Trainer", "valueNames": ["Trainer/Entities/_string"], "entities": [entityID] }); TS_ASSERT_UNEVAL_EQUALS( cmpTrainer.GetEntitiesList(), ["units/iber/c", "units/iber/support_female_citizen", "units/iber/d"] ); TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1), undefined); TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, "units/iber/c"); // Test that we can affect an empty trainer. const emptyTrainer = ConstructComponent(entityID, "Trainer", null); emptyTrainer.OnValueModification({ "component": "Trainer", "entities": [entityID], "valueNames": ["Trainer/Entities/"] }); TS_ASSERT_UNEVAL_EQUALS( emptyTrainer.GetEntitiesList(), ["units/iber/d"] ); Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json (revision 26298) @@ -1,91 +1,88 @@ { "Code": "athen", "Culture": "hele", - "Name": "Athenians", - "Emblem": "session/portraits/emblems/emblem_athenians.png", - "History": "As the cradle of Western civilization and the birthplace of democracy, Athens was famed as a center for the arts, learning and philosophy. The Athenians were also powerful warriors, particularly at sea. At its peak, Athens dominated a large part of the Hellenic world for several decades.", "Music": [ { "File": "Harvest_Festival.ogg", "Type": "peace" }, { "File": "Forging_a_City-State.ogg", "Type": "peace" }, { "File": "Highland_Mist.ogg", "Type": "peace" }, { "File": "The_Hellespont.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Silver Owls", "History": "The mines at Laureion in Attica provided Athens with a wealth of silver from which to mint her famous and highly prized coin, The Athenian Owl.", "Description": "Workers +10% metal gather rate per phase advance." } ], "WallSets": [ "structures/wallset_palisade", "structures/athen/wallset_stone" ], "StartEntities": [ { "Template": "structures/athen/civil_centre" }, { "Template": "units/athen/support_female_citizen", "Count": 4 }, { "Template": "units/athen/infantry_spearman_b", "Count": 2 }, { "Template": "units/athen/infantry_slinger_b", "Count": 2 }, { "Template": "units/athen/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx" ], "AINames": [ "Themistocles", "Pericles", "Cimon", "Aristides", "Xenophon", "Hippias", "Cleisthenes", "Thucydides", "Alcibiades", "Miltiades", "Cleon", "Cleophon", "Thrasybulus", "Iphicrates", "Demosthenes" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/athen/infantry_slinger_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json (revision 26298) @@ -1,89 +1,86 @@ { "Code": "brit", "Culture": "celt", - "Name": "Britons", - "Emblem": "session/portraits/emblems/emblem_britons.png", - "History": "The Britons were the Celtic tribes of the British Isles. Using chariots, longswordsmen and powerful melee soldiers, they staged fearsome revolts against Rome to protect their customs and interests. Also, they built thousands of unique structures such as hill forts, crannogs and brochs.", "Music": [ { "File": "Highland_Mist.ogg", "Type": "peace" }, { "File": "Water's_Edge.ogg", "Type": "peace" }, { "File": "Celtic_Pride.ogg", "Type": "peace" }, { "File": "Cisalpine_Gaul.ogg", "Type": "peace" }, { "File": "Celtica.ogg", "Type": "peace" } ], "CivBonuses": [], "WallSets": [ "structures/wallset_palisade", "structures/brit/wallset_stone" ], "StartEntities": [ { "Template": "structures/brit/civil_centre" }, { "Template": "units/brit/support_female_citizen", "Count": 4 }, { "Template": "units/brit/infantry_spearman_b", "Count": 2 }, { "Template": "units/brit/infantry_slinger_b", "Count": 2 }, { "Template": "units/brit/cavalry_javelineer_b", "Count": 1 }, { "Template": "units/brit/war_dog" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge" ], "AINames": [ "Karatakos", "Kunobelinos", "Boudicca", "Prasutagus", "Venutius", "Cogidubnus", "Commius", "Comux", "Adminius", "Dubnovellaunus", "Vosenius" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/brit/infantry_slinger_b", "skirmish/units/special_starting_unit": "units/brit/war_dog", "skirmish/structures/default_house_5": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 26298) @@ -1,94 +1,91 @@ { "Code": "cart", "Culture": "cart", - "Name": "Carthaginians", - "Emblem": "session/portraits/emblems/emblem_carthaginians.png", - "History": "Carthage, a city-state in modern-day Tunisia, was a formidable force in the western Mediterranean, eventually taking over much of North Africa and modern-day Spain in the third century B.C. The sailors of Carthage were among the fiercest contenders on the high seas, and masters of naval trade. They deployed towered War Elephants on the battlefield to fearsome effect, and had defensive walls so strong, they were never breached.", "Music": [ { "File": "Mediterranean_Waves.ogg", "Type": "peace" }, { "File": "Harsh_Lands_Rugged_People.ogg", "Type": "peace" }, { "File": "Peaks_of_Atlas.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Commercial Acumen", "History": "", "Description": "Merchant Ships +25% trade gain." }, { "Name": "Desirable Products", "History": "", "Description": "Docks and Markets +10% international trade bonus." } ], "WallSets": [ "structures/wallset_palisade", "structures/cart/wallset_short", "structures/cart/wallset_stone" ], "StartEntities": [ { "Template": "structures/cart/civil_centre" }, { "Template": "units/cart/support_female_citizen", "Count": 4 }, { "Template": "units/cart/infantry_spearman_b", "Count": 2 }, { "Template": "units/cart/infantry_archer_b", "Count": 2 }, { "Template": "units/cart/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx" ], "AINames": [ "Hannibal Barca", "Hamilcar Barca", "Hasdrubal Barca", "Hasdrubal Gisco", "Hanno the Elder", "Maharbal", "Mago Barca", "Hasdrubal the Fair", "Hanno the Great", "Himilco", "Hampsicora", "Hannibal Gisco", "Dido", "Xanthippus", "Himilco Phameas", "Hasdrubal the Boetharch" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/cart/infantry_archer_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json (revision 26298) @@ -1,89 +1,86 @@ { "Code": "iber", "Culture": "iber", - "Name": "Iberians", - "Emblem": "session/portraits/emblems/emblem_iberians.png", - "History": "The Iberians were a people of mysterious origins and language, with a strong tradition of horsemanship and metalworking. A relatively peaceful culture, they usually fought in other's battles only as mercenaries. However, they proved tenacious when Rome sought to take their land and freedom from them, and employed pioneering guerrilla tactics and flaming javelins as they fought back.", "Music": [ { "File": "An_old_Warhorse_goes_to_Pasture.ogg", "Type": "peace" }, { "File": "Celtica.ogg", "Type": "peace" }, { "File": "Harsh_Lands_Rugged_People.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Starting Walls", "History": "With exception to alluvial plains and river valleys, stone is abundant in the Iberian Peninsula and was greatly used in construction of structures of all types.", "Description": "Iberians start with City Walls around their base on most maps." }, { "Name": "Massive Towers", "History": "", "Description": "Stone Towers −50% wood cost, +150% stone cost, +33% build time, +60% health, +3 garrison capacity, and +1 default arrow count." } ], "WallSets": [ "structures/wallset_palisade", "structures/iber/wallset_stone" ], "StartEntities": [ { "Template": "structures/iber/civil_centre" }, { "Template": "units/iber/support_female_citizen", "Count": 4 }, { "Template": "units/iber/infantry_swordsman_b", "Count": 2 }, { "Template": "units/iber/infantry_javelineer_b", "Count": 2 }, { "Template": "units/iber/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge" ], "AINames": [ "Viriato", "Viriato", "Karos", "Indibil", "Audax", "Ditalcus", "Minurus", "Tautalus" ], "SkirmishReplacements": { "skirmish/units/default_infantry_melee_b": "units/iber/infantry_swordsman_b", "skirmish/structures/default_house_5": "structures/{civ}/house", "skirmish/structures/iber_wall_short": "structures/iber/wall_short", "skirmish/structures/iber_wall_medium": "structures/iber/wall_medium", "skirmish/structures/iber_wall_long": "structures/iber/wall_long", "skirmish/structures/iber_wall_gate": "structures/iber/wall_gate", "skirmish/structures/iber_wall_tower": "structures/iber/wall_tower" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json (revision 26298) @@ -1,85 +1,82 @@ { "Code": "mace", "Culture": "hele", - "Name": "Macedonians", - "Emblem": "session/portraits/emblems/emblem_macedonians.png", - "History": "Macedonia was an ancient Greek kingdom, centered in the northeastern part of the Greek peninsula. Under the leadership of Alexander the Great, Macedonian forces and allies took over most of the world they knew, including Egypt, Persia and parts of the Indian subcontinent, allowing a diffusion of Hellenic and eastern cultures for years to come.", "Music": [ { "File": "Rise_of_Macedon.ogg", "Type": "peace" }, { "File": "In_the_Shadow_of_Olympus.ogg", "Type": "peace" }, { "File": "Elysian_Fields.ogg", "Type": "peace" }, { "File": "The_Hellespont.ogg", "Type": "peace" } ], "CivBonuses": [], "WallSets": [ "structures/wallset_palisade", "structures/mace/wallset_stone" ], "StartEntities": [ { "Template": "structures/mace/civil_centre" }, { "Template": "units/mace/support_female_citizen", "Count": 4 }, { "Template": "units/mace/infantry_pikeman_b", "Count": 2 }, { "Template": "units/mace/infantry_javelineer_b", "Count": 2 }, { "Template": "units/mace/cavalry_spearman_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Alexander the Great", "Philip II", "Antipater", "Philip IV", "Lysander", "Lysimachus", "Pyrrhus of Epirus", "Antigonus II Gonatas", "Demetrius II Aetolicus", "Philip V", "Perseus", "Craterus", "Meleager" ], "SkirmishReplacements": { "skirmish/units/default_cavalry": "units/mace/cavalry_spearman_b", "skirmish/units/default_infantry_melee_b": "units/mace/infantry_pikeman_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json (revision 26298) @@ -1,93 +1,90 @@ { "Code": "pers", "Culture": "pers", - "Name": "Persians", - "Emblem": "session/portraits/emblems/emblem_persians.png", - "History": "The Persian Empire, when ruled by the Achaemenid dynasty, was one of the greatest empires of antiquity, stretching at its zenith from the Indus Valley in the east to Greece in the west. The Persians were the pioneers of empire-building of the ancient world, successfully imposing a centralized rule over various peoples with different customs, laws, religions and languages, and building a cosmopolitan army made up of contingents from each of these nations.", "Music": [ { "File": "Eastern_Dreams.ogg", "Type": "peace" }, { "File": "Valley_of_the_Nile.ogg", "Type": "peace" }, { "File": "Land_between_the_two_Seas.ogg", "Type": "peace" }, { "File": "Sands_of_Time.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Darics", "History": "", "Description": "Land Traders +25% trade gain." }, { "Name": "Large Rams", "History": "", "Description": "Battering Rams +20% attack damage and +2 garrison capacity." } ], "WallSets": [ "structures/wallset_palisade", "structures/pers/wallset_stone" ], "StartEntities": [ { "Template": "structures/pers/civil_centre" }, { "Template": "units/pers/support_female_citizen", "Count": 4 }, { "Template": "units/pers/infantry_spearman_b", "Count": 2 }, { "Template": "units/pers/infantry_archer_b", "Count": 2 }, { "Template": "units/pers/cavalry_javelineer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/phalanx" ], "AINames": [ "Kurush II the Great", "Darayavahush I", "Cambyses II", "Bardiya", "Xsayarsa I", "Artaxshacha I", "Darayavahush II", "Darayavahush III", "Artaxshacha II", "Artaxshacha III", "Haxamanish", "Xsayarsa II" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/pers/infantry_archer_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json (revision 26298) @@ -1,97 +1,94 @@ { "Code": "rome", "Culture": "rome", - "Name": "Romans", - "Emblem": "session/portraits/emblems/emblem_romans.png", - "History": "The Romans controlled one of the largest empires of the ancient world, stretching at its peak from southern Scotland to the Sahara Desert, and containing between 60 million and 80 million inhabitants, one quarter of the Earth's population at that time. Rome also remained one of the strongest nations on earth for almost 800 years. The Romans were the supreme builders of the ancient world, excelled at siege warfare and had an exquisite infantry and navy.", "Music": [ { "File": "Juno_Protect_You.ogg", "Type": "peace" }, { "File": "Mediterranean_Waves.ogg", "Type": "peace" }, { "File": "Elysian_Fields.ogg", "Type": "peace" }, { "File": "The_Governor.ogg", "Type": "peace" } ], "CivBonuses": [ { "Name": "Testudo Formation", "History": "The Romans commonly used the Testudo or 'turtle' formation for defense: Legionaries were formed into hollow squares with twelve men on each side, standing so close together that their shields overlapped like fish scales.", "Description": "Roman Legionaries can form a Testudo." }, { "Name": "Legionary Engineers", "History": "", "Description": "Battering Rams +20% attack damage. Stone Throwers +10% attack damage." } ], "WallSets": [ "structures/rome/wallset_stone", "structures/rome/wallset_siege" ], "StartEntities": [ { "Template": "structures/rome/civil_centre" }, { "Template": "units/rome/support_female_citizen", "Count": 4 }, { "Template": "units/rome/infantry_swordsman_b", "Count": 2 }, { "Template": "units/rome/infantry_javelineer_b", "Count": 2 }, { "Template": "units/rome/cavalry_spearman_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge", "special/formations/testudo", "special/formations/anti_cavalry" ], "AINames": [ "Lucius Junius Brutus", "Lucius Tarquinius Collatinus", "Gaius Julius Caesar Octavianus", "Marcus Vipsanius Agrippa", "Gaius Iulius Iullus", "Gaius Servilius Structus Ahala", "Publius Cornelius Rufinus", "Lucius Papirius Cursor", "Aulus Manlius Capitolinus", "Publius Cornelius Scipio Africanus", "Publius Sempronius Tuditanus", "Marcus Cornelius Cethegus", "Quintus Caecilius Metellus Pius", "Marcus Licinius Crassus" ], "SkirmishReplacements": { "skirmish/units/default_cavalry": "units/rome/cavalry_spearman_b", "skirmish/units/default_infantry_melee_b": "units/rome/infantry_swordsman_b", "skirmish/structures/default_house_10": "structures/{civ}/house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 26297) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 26298) @@ -1,371 +1,365 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. * settings is the object containing settings for this map. * newPlayers if true will remove old player entities or add new ones until * the new number of player entities is obtained * (used when loading a map or when Atlas changes the number of players). */ function LoadPlayerSettings(settings, newPlayers) { var playerDefaults = Engine.ReadJSONFile("simulation/data/settings/player_defaults.json").PlayerData; // Default settings if (!settings) settings = {}; // Add gaia to simplify iteration // (if gaia is not already the first civ such as when called from Atlas' ActorViewer) if (settings.PlayerData && settings.PlayerData[0] && (!settings.PlayerData[0].Civ || settings.PlayerData[0].Civ != "gaia")) settings.PlayerData.unshift(null); var playerData = settings.PlayerData; // Disable the AIIinterface when no AI players are present if (playerData && !playerData.some(v => v && !!v.AI)) Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface).Disable(); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // Remove existing players or add new ones if (newPlayers) { var settingsNumPlayers = 9; // default 8 players + gaia if (playerData) settingsNumPlayers = playerData.length; // includes gaia (see above) else warn("Player.js: Setup has no player data - using defaults"); while (settingsNumPlayers > numPlayers) { // Add player entity to engine var entID = Engine.AddEntity(GetPlayerTemplateName(getSetting(playerData, playerDefaults, numPlayers, "Civ"))); var cmpPlayer = Engine.QueryInterface(entID, IID_Player); if (!cmpPlayer) throw new Error("Player.js: Error creating player entity " + numPlayers); cmpPlayerManager.AddPlayer(entID); ++numPlayers; } while (settingsNumPlayers < numPlayers) { cmpPlayerManager.RemoveLastPlayer(); --numPlayers; } } // Even when no new player, we must check the template compatibility as player template may be civ dependent for (var i = 0; i < numPlayers; ++i) { var template = GetPlayerTemplateName(getSetting(playerData, playerDefaults, i, "Civ")); var entID = cmpPlayerManager.GetPlayerByID(i); if (cmpTemplateManager.GetCurrentTemplateName(entID) === template) continue; // We need to recreate this player to have the right template entID = Engine.AddEntity(template); cmpPlayerManager.ReplacePlayer(i, entID); } // Initialize the player data for (var i = 0; i < numPlayers; ++i) { - let cmpPlayer = QueryPlayerIDInterface(i); - cmpPlayer.SetName(getSetting(playerData, playerDefaults, i, "Name")); - cmpPlayer.SetCiv(getSetting(playerData, playerDefaults, i, "Civ")); + QueryPlayerIDInterface(i, IID_Identity).SetName(getSetting(playerData, playerDefaults, i, "Name")); var color = getSetting(playerData, playerDefaults, i, "Color"); + const cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetColor(color.r, color.g, color.b); // Special case for gaia if (i == 0) { // Gaia should be its own ally. cmpPlayer.SetAlly(0); // Gaia is everyone's enemy for (var j = 1; j < numPlayers; ++j) cmpPlayer.SetEnemy(j); continue; } // PopulationLimit { let maxPopulation = settings.PlayerData[i].PopulationLimit !== undefined ? settings.PlayerData[i].PopulationLimit : settings.PopulationCap !== undefined ? settings.PopulationCap : playerDefaults[i].PopulationLimit !== undefined ? playerDefaults[i].PopulationLimit : undefined; if (maxPopulation !== undefined) cmpPlayer.SetMaxPopulation(maxPopulation); } // StartingResources if (settings.PlayerData[i].Resources !== undefined) cmpPlayer.SetResourceCounts(settings.PlayerData[i].Resources); else if (settings.StartingResources) { let resourceCounts = cmpPlayer.GetResourceCounts(); let newResourceCounts = {}; for (let resouces in resourceCounts) newResourceCounts[resouces] = settings.StartingResources; cmpPlayer.SetResourceCounts(newResourceCounts); } else if (playerDefaults[i].Resources !== undefined) cmpPlayer.SetResourceCounts(playerDefaults[i].Resources); // DisableSpies if (settings.DisableSpies) { cmpPlayer.AddDisabledTechnology("unlock_spies"); cmpPlayer.AddDisabledTemplate("special/spy"); } // If diplomacy explicitly defined, use that; otherwise use teams if (getSetting(playerData, playerDefaults, i, "Diplomacy") !== undefined) cmpPlayer.SetDiplomacy(getSetting(playerData, playerDefaults, i, "Diplomacy")); else { // Init diplomacy var myTeam = getSetting(playerData, playerDefaults, i, "Team"); // Set all but self as enemies as SetTeam takes care of allies for (var j = 0; j < numPlayers; ++j) { if (i == j) cmpPlayer.SetAlly(j); else cmpPlayer.SetEnemy(j); } cmpPlayer.SetTeam(myTeam === undefined ? -1 : myTeam); } cmpPlayer.SetFormations( getSetting(playerData, playerDefaults, i, "Formations") || Engine.ReadJSONFile("simulation/data/civs/" + cmpPlayer.GetCiv() + ".json").Formations); var startCam = getSetting(playerData, playerDefaults, i, "StartingCamera"); if (startCam !== undefined) cmpPlayer.SetStartingCamera(startCam.Position, startCam.Rotation); } // NOTE: We need to do the team locking here, as otherwise // SetTeam can't ally the players. if (settings.LockTeams) for (let i = 0; i < numPlayers; ++i) QueryPlayerIDInterface(i).SetLockTeams(true); } // Get a setting if it exists or return default function getSetting(settings, defaults, idx, property) { if (settings && settings[idx] && (property in settings[idx])) return settings[idx][property]; // Use defaults if (defaults && defaults[idx] && (property in defaults[idx])) return defaults[idx][property]; return undefined; } function GetPlayerTemplateName(civ) { - let path = "special/player/player"; - - if (Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(path + "_" + civ)) - return path + "_" + civ; - - return path; + return "special/players/" + civ; } /** * @param id An entity's ID * @returns The entity ID of the owner player (not his player ID) or ent if ent is a player entity. */ function QueryOwnerEntityID(ent) { let cmpPlayer = Engine.QueryInterface(ent, IID_Player); if (cmpPlayer) return ent; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return null; return cmpPlayerManager.GetPlayerByID(owner); } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid = IID_Player) { var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; return QueryPlayerIDInterface(owner, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid = IID_Player) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerManager.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but first checks if the entity * mirages the interface. */ function QueryMiragedInterface(ent, iid) { let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage && !cmpMirage.Mirages(iid)) return null; else if (!cmpMirage) return Engine.QueryInterface(ent, iid); return cmpMirage.Get(iid); } /** * Similar to Engine.QueryInterface, but checks for all interfaces * implementing a builder list (currently Foundation and Repairable) * TODO Foundation and Repairable could both implement a BuilderList component */ function QueryBuilderListInterface(ent) { return Engine.QueryInterface(ent, IID_Foundation) || Engine.QueryInterface(ent, IID_Repairable); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsAlly"); } function IsOwnedByMutualAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsMutualAlly"); } function IsOwnedByEntityHelper(entity, target, check) { // Figure out which player controls us let owner = 0; let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); // Figure out which player controls the target entity let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(owner); return cmpPlayer && cmpPlayer[check](targetOwner); } /** * Returns true if the entity 'target' is owned by player */ function IsOwnedByPlayer(player, target) { var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); return cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner(); } function IsOwnedByGaia(target) { return IsOwnedByPlayer(0, target); } /** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsAlly"); } function IsOwnedByMutualAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsMutualAlly"); } function IsOwnedByNeutralOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsNeutral"); } function IsOwnedByEnemyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsEnemy"); } function IsOwnedByHelper(player, target, check) { let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer[check](targetOwner); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Engine.RegisterGlobal("QueryBuilderListInterface", QueryBuilderListInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); Engine.RegisterGlobal("IsOwnedByMutualAllyOfEntity", IsOwnedByMutualAllyOfEntity); Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByGaia", IsOwnedByGaia); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByMutualAllyOfPlayer", IsOwnedByMutualAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByNeutralOfPlayer", IsOwnedByNeutralOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer); Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/brit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/brit.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/brit.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/brit_player_teambonus + + + brit + Britons + The Britons were the Celtic tribes of the British Isles. Using chariots, longswordsmen and powerful melee soldiers, they staged fearsome revolts against Rome to protect their customs and interests. Also, they built thousands of unique structures such as hill forts, crannogs and brochs. + session/portraits/emblems/emblem_britons.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/brit.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaul.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaul.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaul.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/gaul_player_teambonus + + + gaul + Gauls + The Gauls were the Celtic tribes of continental Europe. Dominated by a priestly class of Druids, they featured a sophisticated culture of advanced metalworking, agriculture, trade and even road engineering. With heavy infantry and cavalry, Gallic warriors valiantly resisted Caesar's campaign of conquest and Rome's authoritarian rule. + session/portraits/emblems/emblem_celts.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaul.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml (revision 26298) @@ -0,0 +1,27 @@ + + + + gaia + Gaia + true + + + + + 1.0 + + + 1.0 + 1.0 + 1.0 + 1.0 + + + 1.0 + 1.0 + 1.0 + 1.0 + + + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/kush.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/kush.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/kush.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/kush_player_teambonus + + + kush + Kushites + The Kingdom of Kush was an ancient African kingdom situated on the confluences of the Blue Nile, White Nile and River Atbara in what is now the Republic of Sudan. The Kushite era of rule in the region was established after the Bronze Age collapse of the New Kingdom of Egypt, and it was centered at Napata in its early phase. They invaded Egypt in the 8th century BC, and the Kushite emperors ruled as Pharaohs of the Twenty-fifth dynasty of Egypt for a century, until they were expelled by the Assyrians. Kushite culture was influenced heavily by the Egyptians, with Kushite pyramid building and monumental temple architecture still extent. The Kushites even worshipped many Egyptian gods, including Amun. During Classical antiquity, the Kushite imperial capital was at Meroe. In early Greek geography, the Meroitic kingdom was known as Aethiopia. The Kushite kingdom persisted until the 4th century AD, when it weakened and disintegrated due to internal rebellion, eventually succumbing to the rising power of Axum. + session/portraits/emblems/emblem_kushites.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/kush.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/pers.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/pers.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/pers.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/pers_player_teambonus + + + pers + Persians + The Persian Empire, when ruled by the Achaemenid dynasty, was one of the greatest empires of antiquity, stretching at its zenith from the Indus Valley in the east to Greece in the west. The Persians were the pioneers of empire-building of the ancient world, successfully imposing a centralized rule over various peoples with different customs, laws, religions and languages, and building a cosmopolitan army made up of contingents from each of these nations. + session/portraits/emblems/emblem_persians.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/pers.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/spart.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/spart.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/spart.xml (revision 26298) @@ -0,0 +1,10 @@ + + + teambonuses/spart_player_teambonus + + spart + Spartans + Sparta was a prominent city-state in ancient Greece, and its dominant military power on land from circa 650 B.C. Spartan culture was obsessed with military training and excellence, with rigorous training for boys beginning at age seven. Thanks to its military might, Sparta led a coalition of Greek forces during the Greco-Persian Wars, and won over Athens in the Peloponnesian Wars, though at great cost. + session/portraits/emblems/emblem_spartans.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/spart.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/source/simulation2/Simulation2.h =================================================================== --- ps/trunk/source/simulation2/Simulation2.h (revision 26297) +++ ps/trunk/source/simulation2/Simulation2.h (revision 26298) @@ -1,290 +1,283 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_SIMULATION2 #define INCLUDED_SIMULATION2 #include "lib/file/vfs/vfs_path.h" #include "simulation2/helpers/SimulationCommand.h" #include "simulation2/system/CmpPtr.h" #include "simulation2/system/Components.h" #include #include #include #include class CFrustum; class CMessage; class CSimContext; class CSimulation2Impl; class CTerrain; class CUnitManager; class IComponent; class SceneCollector; class ScriptInterface; class ScriptContext; /** * Public API for simulation system. * Most code should interact with the simulation only through this API. */ class CSimulation2 { NONCOPYABLE(CSimulation2); public: // TODO: CUnitManager should probably be handled automatically by this // module, but for now we'll have it passed in externally instead CSimulation2(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain); ~CSimulation2(); void EnableSerializationTest(); void EnableRejoinTest(int rejoinTestTurn); void EnableOOSLog(); /** * Load all scripts in the specified directory (non-recursively), * so they can register new component types and functions. This * should be called immediately after constructing the CSimulation2 object. * @return false on failure */ bool LoadScripts(const VfsPath& path); /** * Call LoadScripts for each of the game's standard simulation script paths. * @return false on failure */ bool LoadDefaultScripts(); /** * Loads the player settings script (called before map is loaded) * @param newPlayers will delete all the existing player entities (if any) and create new ones * (needed for loading maps, but Atlas might want to update existing player data) */ void LoadPlayerSettings(bool newPlayers); /** * Loads the map settings script (called after map is loaded) */ void LoadMapSettings(); /** * Set a startup script, which will get executed before the first turn. */ void SetStartupScript(const std::string& script); /** * Get the current startup script. */ const std::string& GetStartupScript(); /** * Set the attributes identifying the scenario/RMS used to initialise this * simulation. */ void SetInitAttributes(JS::HandleValue settings); /** * Get the data passed to SetInitAttributes. */ JS::Value GetInitAttributes(); void GetInitAttributes(JS::MutableHandleValue ret); /** * Set the initial map settings (as a UTF-8-encoded JSON string), * which will be used to set up the simulation state. * Called from atlas. */ void SetMapSettings(const std::string& settings); /** * Set the initial map settings, which will be used * to set up the simulation state. * Called from MapReader (for all map-types). */ void SetMapSettings(JS::HandleValue settings); /** * Get the current map settings as a UTF-8 JSON string. */ std::string GetMapSettingsString(); /** * Get the current map settings. */ void GetMapSettings(JS::MutableHandleValue ret); /** * RegMemFun incremental loader function. */ int ProgressiveLoad(); /** * Reload any scripts that were loaded from the given filename. * (This is used to implement hotloading.) */ Status ReloadChangedFile(const VfsPath& path); /** * Initialise (or re-initialise) the complete simulation state. * Must be called after LoadScripts, and must be called * before any methods that depend on the simulation state. * @param skipScriptedComponents don't load the scripted system components * (this is intended for use by test cases that don't mount all of VFS) * @param skipAI don't initialise the AI system * (this is intended for use by test cases that don't want all entity * templates loaded automatically) */ void ResetState(bool skipScriptedComponents = false, bool skipAI = false); /** * Replace/destroy some entities (e.g. skirmish replacers) * Called right before InitGame, on CGame instantiation. * (This mustn't be used when e.g. loading saved games, only when starting new ones.) * This calls the PreInitGame function defined in helpers/InitGame.js. */ void PreInitGame(); /** * Initialise a new game, based on some script data. (Called on CGame instantiation) * (This mustn't be used when e.g. loading saved games, only when starting new ones.) * This calls the InitGame function defined in helpers/InitGame.js. */ void InitGame(); void Update(int turnLength); void Update(int turnLength, const std::vector& commands); void Interpolate(float simFrameLength, float frameOffset, float realFrameLength); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); /** * Returns the last frame offset passed to Interpolate(), i.e. the offset corresponding * to the currently-rendered scene. */ float GetLastFrameOffset() const; /** * Construct a new entity and add it to the world. * @param templateName see ICmpTemplateManager for syntax * @return the new entity ID, or INVALID_ENTITY on error */ entity_id_t AddEntity(const std::wstring& templateName); entity_id_t AddEntity(const std::wstring& templateName, entity_id_t preferredId); entity_id_t AddLocalEntity(const std::wstring& templateName); /** * Destroys the specified entity, once FlushDestroyedEntities is called. * Has no effect if the entity does not exist, or has already been added to the destruction queue. */ void DestroyEntity(entity_id_t ent); /** * Does the actual destruction of entities from DestroyEntity. * This is called automatically by Update, but should also be called at other * times when an entity might have been deleted and should be removed from * any further processing (e.g. after editor UI message processing) */ void FlushDestroyedEntities(); IComponent* QueryInterface(entity_id_t ent, int iid) const; void PostMessage(entity_id_t ent, const CMessage& msg) const; void BroadcastMessage(const CMessage& msg) const; using InterfaceList = std::vector >; using InterfaceListUnordered = std::unordered_map; /** * Returns a list of components implementing the given interface, and their * associated entities, sorted by entity ID. */ InterfaceList GetEntitiesWithInterface(int iid); /** * Returns a list of components implementing the given interface, and their * associated entities, as an unordered map. */ const InterfaceListUnordered& GetEntitiesWithInterfaceUnordered(int iid); const CSimContext& GetSimContext() const; ScriptInterface& GetScriptInterface() const; bool ComputeStateHash(std::string& outHash, bool quick); bool DumpDebugState(std::ostream& stream); bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); /** * Activate the rejoin-test feature for turn @param turn. */ void ActivateRejoinTest(int turn); std::string GenerateSchema(); ///////////////////////////////////////////////////////////////////////////// // Some functions for Atlas UI to be able to access VFS data /** * Get random map script data * * @return vector of strings containing JSON format data */ std::vector GetRMSData(); /** - * Get civilization data - * - * @return vector of strings containing JSON format data - */ - std::vector GetCivData(); - - /** * Get victory condition data * * @return vector of strings containing JSON format data */ std::vector GetVictoryConditiondData(); /** * Get player default data * * @return string containing JSON format data */ std::string GetPlayerDefaults(); /** * Get map sizes data * * @return string containing JSON format data */ std::string GetMapSizes(); /** * Get AI data * * @return string containing JSON format data */ std::string GetAIData(); private: CSimulation2Impl* m; }; #endif // INCLUDED_SIMULATION2 Index: ps/trunk/source/simulation2/components/ICmpIdentity.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpIdentity.cpp (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpIdentity.cpp (revision 26298) @@ -1,45 +1,50 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpIdentity.h" #include "simulation2/system/InterfaceScripted.h" #include "simulation2/scripting/ScriptComponent.h" BEGIN_INTERFACE_WRAPPER(Identity) END_INTERFACE_WRAPPER(Identity) class CCmpIdentityScripted : public ICmpIdentity { public: DEFAULT_SCRIPT_WRAPPER(IdentityScripted) virtual std::string GetSelectionGroupName() { return m_Script.Call("GetSelectionGroupName"); } virtual std::wstring GetPhenotype() { return m_Script.Call("GetPhenotype"); } + + virtual std::wstring GetCiv() + { + return m_Script.Call("GetCiv"); + } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(IdentityScripted) Index: ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp (revision 26298) @@ -1,31 +1,32 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpTemplateManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(TemplateManager) DEFINE_INTERFACE_METHOD("GetTemplate", ICmpTemplateManager, GetTemplate) DEFINE_INTERFACE_METHOD("GetTemplateWithoutValidation", ICmpTemplateManager, GetTemplateWithoutValidation) DEFINE_INTERFACE_METHOD("TemplateExists", ICmpTemplateManager, TemplateExists) DEFINE_INTERFACE_METHOD("GetCurrentTemplateName", ICmpTemplateManager, GetCurrentTemplateName) DEFINE_INTERFACE_METHOD("FindAllTemplates", ICmpTemplateManager, FindAllTemplates) +DEFINE_INTERFACE_METHOD("GetCivData", ICmpTemplateManager, GetCivData) DEFINE_INTERFACE_METHOD("GetEntitiesUsingTemplate", ICmpTemplateManager, GetEntitiesUsingTemplate) END_INTERFACE_WRAPPER(TemplateManager) Index: ps/trunk/source/tools/atlas/GameInterface/Messages.h =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Messages.h (revision 26297) +++ ps/trunk/source/tools/atlas/GameInterface/Messages.h (revision 26298) @@ -1,745 +1,745 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MESSAGES #define INCLUDED_MESSAGES #ifndef MESSAGES_SKIP_SETUP #include "MessagesSetup.h" #endif #include #include // TODO: organisation, documentation, etc #ifdef _MSC_VER // (can't use MSC_VERSION here since this file is included by Atlas too) #pragma warning(push) #pragma warning(disable: 4003) #endif ////////////////////////////////////////////////////////////////////////// // Initialise some engine code. Must be called before anything else. MESSAGE(Init, ); MESSAGE(InitAppWindow, ((void*, handle)) // Atlas Window handle. ); // Initialise SDL-related code. Must be called before SetCanvas and InitGraphics. MESSAGE(InitSDL, ); // Initialise graphics-related code. Must be called after the first SetCanvas, // and before much else. MESSAGE(InitGraphics, ); // Shut down engine/graphics code. MESSAGE(Shutdown, ); struct eRenderView { enum renderViews { NONE, GAME, ACTOR }; }; MESSAGE(RenderEnable, ((int, view)) // eRenderView ); // SetViewParam: used for hints to the renderer, e.g. to set wireframe mode; // unrecognised param names are ignored MESSAGE(SetViewParamB, ((int, view)) // eRenderView ((std::wstring, name)) ((bool, value)) ); MESSAGE(SetViewParamI, ((int, view)) // eRenderView ((std::wstring, name)) ((int, value)) ); MESSAGE(SetViewParamC, ((int, view)) // eRenderView ((std::wstring, name)) ((Color, value)) ); MESSAGE(SetViewParamS, ((int, view)) // eRenderView ((std::wstring, name)) ((std::wstring, value)) ); MESSAGE(JavaScript, ((std::string, command)) ); ////////////////////////////////////////////////////////////////////////// MESSAGE(GuiSwitchPage, ((std::wstring, page)) ); MESSAGE(GuiMouseButtonEvent, ((int, button)) ((bool, pressed)) ((Position, pos)) ((int, clicks)) ); MESSAGE(GuiMouseMotionEvent, ((Position, pos)) ); MESSAGE(GuiKeyEvent, ((int, sdlkey)) // SDLKey code ((int, unichar)) // Unicode character ((bool, pressed)) ); MESSAGE(GuiCharEvent, ((int, sdlkey)) ((int, unichar)) ); ////////////////////////////////////////////////////////////////////////// MESSAGE(SimStopMusic, ); MESSAGE(SimStateSave, ((std::wstring, label)) // named slot to store saved data ); MESSAGE(SimStateRestore, ((std::wstring, label)) // named slot to find saved data ); QUERY(SimStateDebugDump, ((bool, binary)) , ((std::wstring, dump)) ); MESSAGE(SimPlay, ((float, speed)) // 0 for pause, 1 for normal speed ((bool, simTest)) // true if we're in simulation test mode, false otherwise ); ////////////////////////////////////////////////////////////////////////// QUERY(Ping, , ); ////////////////////////////////////////////////////////////////////////// MESSAGE(SetCanvas, ((void*, canvas)) ((int, width)) ((int, height)) ); MESSAGE(ResizeScreen, ((int, width)) ((int, height)) ); QUERY(RenderLoop, , ((bool, wantHighFPS)) ((double, timeSinceActivity)) ); ////////////////////////////////////////////////////////////////////////// // Messages for map panel QUERY(GenerateMap, ((std::wstring, filename)) // random map script filename ((std::string, settings)) // map settings as JSON string , ((int, status)) ); MESSAGE(ImportHeightmap, ((std::wstring, filename)) ); MESSAGE(LoadMap, ((std::wstring, filename)) ); MESSAGE(SaveMap, ((std::wstring, filename)) ); QUERY(GetMapList, , ((std::vector, scenarioFilenames)) ((std::vector, skirmishFilenames)) ((std::vector, tutorialFilenames)) ); QUERY(GetMapSettings, , ((std::string, settings)) ); COMMAND(SetMapSettings, MERGE, ((std::string, settings)) ); MESSAGE(LoadPlayerSettings, ((bool, newplayers)) ); QUERY(GetMapSizes, , ((std::string, sizes)) ); QUERY(GetCurrentMapSize, , ((int, size)) ); QUERY(RasterizeMinimap, , ((int, dimension)) ((std::vector, imageBytes)) ); QUERY(GetRMSData, , ((std::vector, data)) ); COMMAND(ResizeMap, NOMERGE, ((int, tiles)) ((int, offsetX)) ((int, offsetY)) ); QUERY(VFSFileExists, ((std::wstring, path)) , ((bool, exists)) ); QUERY(VFSFileRealPath, ((std::wstring, path)) , ((std::wstring, realPath)) ); ////////////////////////////////////////////////////////////////////////// // Messages for player panel QUERY(GetCivData, , - ((std::vector, data)) + ((std::vector>, data)) ); QUERY(GetVictoryConditionData, , ((std::vector, data)) ); QUERY(GetPlayerDefaults, , ((std::string, defaults)) ); QUERY(GetAIData, , ((std::string, data)) ); ////////////////////////////////////////////////////////////////////////// MESSAGE(RenderStyle, ((bool, wireframe)) ); MESSAGE(MessageTrace, ((bool, enable)) ); MESSAGE(Screenshot, ((bool, big)) ); ////////////////////////////////////////////////////////////////////////// MESSAGE(Brush, ((int, width)) // number of vertices ((int, height)) ((std::vector, data)) // width*height array ); MESSAGE(BrushPreview, ((bool, enable)) ((Position, pos)) // only used if enable==true ); ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// QUERY(GetTerrainGroups, , // no inputs ((std::vector, groupNames)) ); QUERY(GetTerrainGroupTextures, ((std::wstring, groupName)), ((std::vector, names)) ); #ifndef MESSAGES_SKIP_STRUCTS struct sTerrainTexturePreview { Shareable name; Shareable loaded; Shareable imageWidth; Shareable imageHeight; Shareable> imageData; // RGB*width*height }; SHAREABLE_STRUCT(sTerrainTexturePreview); #endif QUERY(GetTerrainGroupPreviews, ((std::wstring, groupName)) ((int, imageWidth)) ((int, imageHeight)) , ((std::vector, previews)) ); QUERY(GetTerrainPassabilityClasses, , // no inputs ((std::vector, classNames)) ); QUERY(GetTerrainTexturePreview, ((std::wstring, name)) ((int, imageWidth)) ((int, imageHeight)) , ((sTerrainTexturePreview, preview)) ); ////////////////////////////////////////////////////////////////////////// #ifndef MESSAGES_SKIP_STRUCTS struct sObjectsListItem { Shareable id; Shareable name; Shareable type; // 0 = entity, 1 = actor }; SHAREABLE_STRUCT(sObjectsListItem); #endif QUERY(GetObjectsList, , // no inputs ((std::vector, objects)) // sorted by .name ); #ifndef MESSAGES_SKIP_STRUCTS struct sObjectSettings { Shareable player; Shareable > selections; // Some settings are immutable and therefore are ignored (and should be left // empty) when passed from the editor to the game: Shareable > > variantGroups; }; SHAREABLE_STRUCT(sObjectSettings); #endif // transform de local entity to a real entity MESSAGE(ObjectPreviewToEntity,); //Query for get selected objects QUERY(GetCurrentSelection, , //No inputs ((std::vector, ids)) ); // Moving Preview(s) object together, default is using the firs element in vector MESSAGE(MoveObjectPreview, ((Position,pos)) ); // Preview object in the game world - creates a temporary unit at the given // position, and removes it when the preview is next changed MESSAGE(ObjectPreview, ((std::wstring, id)) // or empty string => disable ((sObjectSettings, settings)) ((Position, pos)) ((bool, usetarget)) // true => use 'target' for orientation; false => use 'angle' ((Position, target)) ((float, angle)) ((unsigned int, actorseed)) ((bool, cleanObjectPreviews)) ); COMMAND(CreateObject, NOMERGE, ((std::wstring, id)) ((sObjectSettings, settings)) ((Position, pos)) ((bool, usetarget)) // true => use 'target' for orientation; false => use 'angle' ((Position, target)) ((float, angle)) ((unsigned int, actorseed)) ); // Set an actor to be previewed on its own (i.e. without the game world). // (Use RenderEnable to make it visible.) MESSAGE(SetActorViewer, ((std::wstring, id)) ((std::string, animation)) ((int, playerID)) ((float, speed)) ((bool, flushcache)) // true => unload all actor files before starting the preview (because we don't have proper hotloading yet) ); ////////////////////////////////////////////////////////////////////////// QUERY(Exit,,); // no inputs nor outputs ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// struct eScrollConstantDir { enum { FORWARDS, BACKWARDS, LEFT, RIGHT, CLOCKWISE, ANTICLOCKWISE }; }; MESSAGE(ScrollConstant, // set a constant scrolling(/rotation) rate ((int, view)) // eRenderView ((int, dir)) // eScrollConstantDir ((float, speed)) // set speed 0.0f to stop scrolling ); struct eScrollType { enum { FROM, TO }; }; MESSAGE(Scroll, // for scrolling by dragging the mouse FROM somewhere TO elsewhere ((int, view)) // eRenderView ((int, type)) // eScrollType ((Position, pos)) ); MESSAGE(SmoothZoom, ((int, view)) // eRenderView ((float, amount)) ); struct eRotateAroundType { enum { FROM, TO }; }; MESSAGE(RotateAround, ((int, view)) // eRenderView ((int, type)) // eRotateAroundType ((Position, pos)) ); MESSAGE(LookAt, ((int, view)) // eRenderView ((Position, pos)) ((Position, target)) ); MESSAGE(CameraReset, ); QUERY(GetView, , ((sCameraInfo, info)) ); MESSAGE(SetView, ((sCameraInfo, info)) ); ////////////////////////////////////////////////////////////////////////// #ifndef MESSAGES_SKIP_STRUCTS struct sEnvironmentSettings { Shareable watertype; // range 0..1 corresponds to min..max terrain height; out-of-bounds values allowed Shareable waterheight; // range 0..1 corresponds to min..max terrain height; out-of-bounds values allowed Shareable waterwaviness; // range ??? Shareable watermurkiness; // range ??? Shareable windangle; Shareable watercolor; Shareable watertint; Shareable sunrotation; // range -pi..+pi Shareable sunelevation; // range -pi/2 .. +pi/2 // emulate 'HDR' by allowing overly bright suncolor. this is // multiplied on to suncolor after converting to float // (struct Color stores as normal u8, 0..255) Shareable sunoverbrightness; // range 1..3 // support different lighting models ("old" for the version compatible with old scenarios, // "standard" for the new normal model that supports much brighter lighting) Shareable posteffect; Shareable skyset; Shareable suncolor; Shareable ambientcolor; Shareable fogcolor; Shareable fogfactor; Shareable fogmax; Shareable brightness; Shareable contrast; Shareable saturation; Shareable bloom; }; SHAREABLE_STRUCT(sEnvironmentSettings); #endif QUERY(GetEnvironmentSettings, // no inputs , ((sEnvironmentSettings, settings)) ); COMMAND(SetEnvironmentSettings, MERGE, // merge lots of small changes into one undoable command ((sEnvironmentSettings, settings)) ); COMMAND(RecalculateWaterData, NOMERGE, ((float, unused))); COMMAND(PickWaterHeight, NOMERGE, ((Position, screenPos))); QUERY(GetSkySets, // no inputs , ((std::vector, skysets)) ); QUERY(GetPostEffects, // no inputs , ((std::vector, posteffects)) ); ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// COMMAND(AlterElevation, MERGE, ((Position, pos)) ((float, amount)) ); COMMAND(SmoothElevation, MERGE, ((Position, pos)) ((float, amount)) ); COMMAND(FlattenElevation, MERGE, ((Position, pos)) ((float, amount)) ); COMMAND(PikeElevation, MERGE, ((Position, pos)) ((float, amount)) ); struct ePaintTerrainPriority { enum { HIGH, LOW }; }; COMMAND(PaintTerrain, MERGE, ((Position, pos)) ((std::wstring, texture)) ((int, priority)) // ePaintTerrainPriority ); COMMAND(ReplaceTerrain, NOMERGE, ((Position, pos)) ((std::wstring, texture)) ); COMMAND(FillTerrain, NOMERGE, ((Position, pos)) ((std::wstring, texture)) ); QUERY(GetTerrainTexture, ((Position, pos)) , ((std::wstring, texture)) ); ////////////////////////////////////////////////////////////////////////// QUERY(PickObject, ((Position, pos)) ((bool, selectActors)) , ((ObjectID, id)) ((int, offsetx)) // offset of object centre from input position ((int, offsety)) // ); QUERY(PickObjectsInRect, ((Position, start)) ((Position, end)) ((bool, selectActors)) , ((std::vector, ids)) ); QUERY(PickSimilarObjects, ((ObjectID, id)) , ((std::vector, ids)) ); MESSAGE(ResetSelectionColor, ); COMMAND(MoveObjects, MERGE, ((std::vector, ids)) ((ObjectID, pivot)) ((Position, pos)) ); COMMAND(RotateObjectsFromCenterPoint, MERGE, ((std::vector, ids)) ((Position, target)) ((bool, rotateObject)) ); COMMAND(RotateObject, MERGE, ((std::vector, ids)) ((Position, target)) ); COMMAND(DeleteObjects, NOMERGE, ((std::vector, ids)) ); MESSAGE(SetSelectionPreview, ((std::vector, ids)) ); QUERY(GetObjectSettings, ((int, view)) // eRenderView ((ObjectID, id)) , ((sObjectSettings, settings)) ); COMMAND(SetObjectSettings, NOMERGE, ((int, view)) // eRenderView ((ObjectID, id)) ((sObjectSettings, settings)) ); QUERY(GetObjectMapSettings, ((std::vector, ids)) , ((std::wstring, xmldata)) ); QUERY(GetPlayerObjects, ((int, player)) , ((std::vector, ids)) ); MESSAGE(SetBandbox, ((bool, show)) ((int, sx0)) ((int, sy0)) ((int, sx1)) ((int, sy1)) ); ////////////////////////////////////////////////////////////////////////// QUERY(GetCinemaPaths, , // no inputs ((std::vector , paths)) ); QUERY(GetCameraInfo, , ((AtlasMessage::sCameraInfo, info)) ); QUERY(PickPathNode, ((Position, pos)) , ((AtlasMessage::sCinemaPathNode, node)) ); QUERY(PickAxis, ((AtlasMessage::sCinemaPathNode, node)) ((Position, pos)) , ((int, axis)) ); COMMAND(AddPathNode, NOMERGE, ((AtlasMessage::sCinemaPathNode, node)) ); COMMAND(DeletePathNode, NOMERGE, ((AtlasMessage::sCinemaPathNode, node)) ); COMMAND(MovePathNode, NOMERGE, ((AtlasMessage::sCinemaPathNode, node)) ((int, axis)) ((Position, from)) ((Position, to)) ); COMMAND(AddCinemaPath, NOMERGE, ((std::wstring, pathName))); COMMAND(DeleteCinemaPath, NOMERGE, ((std::wstring, pathName))); COMMAND(SetCinemaPaths, NOMERGE, ((std::vector, paths)) ); COMMAND(SetCinemaPathsDrawing, NOMERGE, ((bool, drawPaths))); MESSAGE(CinemaEvent, ((std::wstring, path)) ((int, mode)) ((float, t)) ((bool, drawCurrent)) ((bool, lines)) ); MESSAGE(ClearPathNodePreview,); ////////////////////////////////////////////////////////////////////////// QUERY(GetSelectedObjectsTemplateNames, ((std::vector, ids)) , ((std::vector, names)) ); #ifdef _MSC_VER #pragma warning(pop) #endif #ifndef MESSAGES_SKIP_SETUP #include "MessagesSetup.h" #endif #endif // INCLUDED_MESSAGES Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/maur.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/maur.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/maur.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/maur_player_teambonus + + + maur + Mauryas + Founded in 322 B.C. by Chandragupta Maurya, the Mauryan Empire was the first to rule most of the Indian subcontinent, and was one of the largest and most populous empires of antiquity. Its military featured bowmen who used the long-range bamboo longbow, fierce female warriors, chariots, and thousands of armored war elephants. Its philosophers, especially the famous Acharya Chanakya, contributed to such varied fields such as economics, religion, diplomacy, warfare, and good governance. Under the rule of Ashoka the Great, the empire saw 40 years of peace, harmony, and prosperity. + session/portraits/emblems/emblem_mauryas.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/maur.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/sele.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/sele.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/sele.xml (revision 26298) @@ -0,0 +1,20 @@ + + + + teambonuses/sele_player_teambonus + + + + + phase_town + Hero + + + + + sele + Seleucids + The Macedonian-Greek dynasty that ruled most of Alexander's former empire. + session/portraits/emblems/emblem_seleucids.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/sele.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 26297) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 26298) @@ -1,1001 +1,996 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Simulation2.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "scriptinterface/StructuredClone.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_util.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/Util.h" #include "ps/XML/Xeromyces.h" #include #include #include class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain) : m_SimContext(), m_ComponentManager(m_SimContext, cx), m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false), m_MapSettings(cx->GetGeneralJSContext()), m_InitAttributes(cx->GetGeneralJSContext()) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); RegisterFileReloadFunc(ReloadChangedFileCB, this); // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("ooslog", m_EnableOOSLog); CFG_GET_VAL("serializationtest", m_EnableSerializationTest); CFG_GET_VAL("rejointest", m_RejoinTestTurn); if (m_RejoinTestTurn < 0) // Handle bogus values of the arg m_RejoinTestTurn = -1; } if (m_EnableOOSLog) { m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m_OOSLogPath.string8().c_str()); } } ~CSimulation2Impl() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } void ResetState(bool skipScriptedComponents, bool skipAI) { m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI); } static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI) { componentManager.ResetState(); componentManager.InitSystemEntity(); componentManager.AddSystemComponents(skipScriptedComponents, skipAI); } static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts); static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path); static bool LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts); Status ReloadChangedFile(const VfsPath& path); static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } int ProgressiveLoad(); void Update(int turnLength, const std::vector& commands); static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands); void Interpolate(float simFrameLength, float frameOffset, float realFrameLength); void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; double m_DeltaTime; float m_LastFrameOffset; std::string m_StartupScript; JS::PersistentRootedValue m_InitAttributes; JS::PersistentRootedValue m_MapSettings; std::set m_LoadedScripts; uint32_t m_TurnNumber; bool m_EnableOOSLog; OsPath m_OOSLogPath; // Functions and data for the serialization test mode: (see Update() for relevant comments) bool m_EnableSerializationTest; int m_RejoinTestTurn; bool m_TestingRejoin; // Secondary simulation (NB: order matters for destruction). std::unique_ptr m_SecondaryComponentManager; std::unique_ptr m_SecondaryTerrain; std::unique_ptr m_SecondaryContext; std::unique_ptr> m_SecondaryLoadedScripts; struct SerializationTestState { std::stringstream state; std::stringstream debug; std::string hash; }; void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix); void ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter); void InitRNGSeedSimulation(); void InitRNGSeedAI(); static std::vector CloneCommandsFromOtherCompartment(const ScriptInterface& newScript, const ScriptInterface& oldScript, const std::vector& commands) { std::vector newCommands; newCommands.reserve(commands.size()); ScriptRequest rqNew(newScript); for (const SimulationCommand& command : commands) { JS::RootedValue tmpCommand(rqNew.cx, Script::CloneValueFromOtherCompartment(newScript, oldScript, command.data)); Script::FreezeObject(rqNew, tmpCommand, true); SimulationCommand cmd(command.player, rqNew.cx, tmpCommand); newCommands.emplace_back(std::move(cmd)); } return newCommands; } }; bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts) { return ( LoadScripts(componentManager, loadedScripts, L"simulation/components/interfaces/") && LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") && LoadScripts(componentManager, loadedScripts, L"simulation/components/") ); } bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path) { VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) return false; bool ok = true; for (const VfsPath& scriptPath : pathnames) { if (loadedScripts) loadedScripts->insert(scriptPath); LOGMESSAGE("Loading simulation script '%s'", scriptPath.string8()); if (!componentManager.LoadScript(scriptPath)) ok = false; } return ok; } bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts) { bool ok = true; ScriptRequest rq(componentManager.GetScriptInterface()); if (Script::HasProperty(rq, mapSettings, "TriggerScripts")) { std::vector scriptNames; Script::GetProperty(rq, mapSettings, "TriggerScripts", scriptNames); for (const std::string& triggerScript : scriptNames) { std::string scriptName = "maps/" + triggerScript; if (loadedScripts) { if (loadedScripts->find(scriptName) != loadedScripts->end()) continue; loadedScripts->insert(scriptName); } LOGMESSAGE("Loading trigger script '%s'", scriptName.c_str()); if (!componentManager.LoadScript(scriptName.data())) ok = false; } } return ok; } Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path) { // Ignore if this file wasn't loaded as a script // (TODO: Maybe we ought to load in any new .js files that are created in the right directories) if (m_LoadedScripts.find(path) == m_LoadedScripts.end()) return INFO::OK; // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message. // (Also don't bother trying to 'unload' it from the component manager, because that's not possible) if (!VfsFileExists(path)) return INFO::OK; LOGMESSAGE("Reloading simulation script '%s'", path.string8()); if (!m_ComponentManager.LoadScript(path, true)) return ERR::FAIL; return INFO::OK; } int CSimulation2Impl::ProgressiveLoad() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; int ret; do { bool progressed = false; int total = 0; int progress = 0; CMessageProgressiveLoad msg(&progressed, &total, &progress); m_ComponentManager.BroadcastMessage(msg); if (!progressed || total == 0) return 0; // we have nothing left to load ret = Clamp(100*progress / total, 1, 100); } while (timer_Time() < end_time); return ret; } void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix) { if (!state.hash.empty()) { std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << Hexify(state.hash); } if (!state.debug.str().empty()) { std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << state.debug.str(); } if (!state.state.str().empty()) { std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); file << state.state.str(); } } void CSimulation2Impl::ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter) { const OsPath path = createDateIndexSubdirectory(psLogDir() / "serializationtest"); debug_printf("Writing serializationtest-data to %s\n", path.string8().c_str()); // Clean up obsolete files from previous runs wunlink(path / "hash.before.a"); wunlink(path / "hash.before.b"); wunlink(path / "debug.before.a"); wunlink(path / "debug.before.b"); wunlink(path / "state.before.a"); wunlink(path / "state.before.b"); wunlink(path / "hash.after.a"); wunlink(path / "hash.after.b"); wunlink(path / "debug.after.a"); wunlink(path / "debug.after.b"); wunlink(path / "state.after.a"); wunlink(path / "state.after.b"); if (primaryStateBefore) DumpSerializationTestState(*primaryStateBefore, path, L"before.a"); if (primaryStateAfter) DumpSerializationTestState(*primaryStateAfter, path, L"after.a"); if (secondaryStateBefore) DumpSerializationTestState(*secondaryStateBefore, path, L"before.b"); if (secondaryStateAfter) DumpSerializationTestState(*secondaryStateAfter, path, L"after.b"); debug_warn(L"Serialization test failure"); } void CSimulation2Impl::InitRNGSeedSimulation() { u32 seed = 0; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "Seed") || !Script::GetProperty(rq, m_MapSettings, "Seed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed); m_ComponentManager.SetRNGSeed(seed); } void CSimulation2Impl::InitRNGSeedAI() { u32 seed = 0; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "AISeed") || !Script::GetProperty(rq, m_MapSettings, "AISeed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedAI: No seed value specified - using %d", seed); CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->SetRNGSeed(seed); } void CSimulation2Impl::Update(int turnLength, const std::vector& commands) { PROFILE3("sim update"); PROFILE2_ATTR("turn %d", (int)m_TurnNumber); fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; /* * In serialization test mode, we save the original (primary) simulation state before each turn update. * We run the update, then load the saved state into a secondary context. * We serialize that again and compare to the original serialization (to check that * serialize->deserialize->serialize is equivalent to serialize). * Then we run the update on the secondary context, and check that its new serialized * state matches the primary context after the update (to check that the simulation doesn't depend * on anything that's not serialized). * * In rejoin test mode, the secondary simulation is initialized from serialized data at turn N, then both * simulations run independantly while comparing their states each turn. This is way faster than a * complete serialization test and allows us to reproduce OOSes on rejoin. */ const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow) const bool serializationTestHash = true; // set true to save and compare hash of state SerializationTestState primaryStateBefore; const ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface(); const bool startRejoinTest = (int64_t) m_RejoinTestTurn == m_TurnNumber; if (startRejoinTest) m_TestingRejoin = true; if (m_EnableSerializationTest || m_TestingRejoin) { ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false)); } UpdateComponents(m_SimContext, turnLengthFixed, commands); if (m_EnableSerializationTest || startRejoinTest) { if (startRejoinTest) debug_printf("Initializing the secondary simulation\n"); m_SecondaryTerrain = std::make_unique(); m_SecondaryContext = std::make_unique(); m_SecondaryContext->m_Terrain = m_SecondaryTerrain.get(); m_SecondaryComponentManager = std::make_unique(*m_SecondaryContext, scriptInterface.GetContext()); m_SecondaryComponentManager->LoadComponentTypes(); m_SecondaryLoadedScripts = std::make_unique>(); ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts.get())); ResetComponentState(*m_SecondaryComponentManager, false, false); ScriptRequest rq(scriptInterface); // Load the trigger scripts after we have loaded the simulation. { ScriptRequest rq2(m_SecondaryComponentManager->GetScriptInterface()); JS::RootedValue mapSettingsCloned(rq2.cx, Script::CloneValueFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, m_MapSettings)); ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts.get())); } // Load the map into the secondary simulation LDR_BeginRegistering(); std::unique_ptr mapReader = std::make_unique(); std::string mapType; Script::GetProperty(rq, m_InitAttributes, "mapType", mapType); if (mapType == "random") { // TODO: support random map scripts debug_warn(L"Serialization test mode does not support random maps"); } else { std::wstring mapFile; Script::GetProperty(rq, m_InitAttributes, "map", mapFile); VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp"); mapReader->LoadMap(mapfilename, *scriptInterface.GetContext(), JS::UndefinedHandleValue, m_SecondaryTerrain.get(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, m_SecondaryContext.get(), INVALID_PLAYER, true); // throws exception on failure } LDR_EndRegistering(); ENSURE(LDR_NonprogressiveLoad() == INFO::OK); ENSURE(m_SecondaryComponentManager->DeserializeState(primaryStateBefore.state)); } if (m_EnableSerializationTest || m_TestingRejoin) { SerializationTestState secondaryStateBefore; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateBefore.hash, false)); if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() || primaryStateBefore.hash != secondaryStateBefore.hash) { ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL); } SerializationTestState primaryStateAfter; ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false)); UpdateComponents(*m_SecondaryContext, turnLengthFixed, CloneCommandsFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, commands)); SerializationTestState secondaryStateAfter; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateAfter.state)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateAfter.hash, false)); if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() || primaryStateAfter.hash != secondaryStateAfter.hash) { // Only do the (slow) dumping now we know we're going to need to report it ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false)); ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateAfter.debug, false)); ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter); } } // Run the GC occasionally // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are // much more turns in the same time than in normal games. // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code. // Based on testing, this seems to be a good compromise between memory usage and performance. // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic: // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323 // // (TODO: we ought to schedule this for a frame where we're not // running the sim update, to spread the load) if (m_TurnNumber % 500 == 0) scriptInterface.GetContext()->ShrinkingGC(); else scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState(); ++m_TurnNumber; } void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands) { // TODO: the update process is pretty ugly, with lots of messages and dependencies // between different components. Ought to work out a nicer way to do this. CComponentManager& componentManager = simContext.GetComponentManager(); CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY); if (cmpPathfinder) cmpPathfinder->SendRequestedPaths(); { PROFILE2("Sim - Update Start"); CMessageTurnStart msgTurnStart; componentManager.BroadcastMessage(msgTurnStart); } CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); if (cmpCommandQueue) cmpCommandQueue->FlushTurn(commands); // Process newly generated move commands so the UI feels snappy if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } // Send all the update phases { PROFILE2("Sim - Update"); CMessageUpdate msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Process move commands for formations (group proxy) if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } { PROFILE2("Sim - Motion Unit"); CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { PROFILE2("Sim - Update Final"); CMessageUpdate_Final msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Clean up any entities destroyed during the simulation update componentManager.FlushDestroyedComponents(); // Compute AI immediately at turn's end. CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); if (cmpAIManager) { cmpAIManager->StartComputation(); cmpAIManager->PushCommands(); } // Process all remaining moves if (cmpPathfinder) { cmpPathfinder->UpdateGrid(); cmpPathfinder->StartProcessingMoves(false); } } void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { PROFILE3("sim interpolate"); m_LastFrameOffset = frameOffset; CMessageInterpolate msg(simFrameLength, frameOffset, realFrameLength); m_ComponentManager.BroadcastMessage(msg); // Clean up any entities destroyed during interpolate (e.g. local corpses) m_ComponentManager.FlushDestroyedComponents(); } void CSimulation2Impl::DumpState() { PROFILE("DumpState"); std::stringstream name;\ name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt"; const OsPath path = m_OOSLogPath / name.str(); std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (!DirectoryExists(m_OOSLogPath)) { LOGWARNING("OOS-log directory %s was deleted, creating it again.", m_OOSLogPath.string8().c_str()); CreateDirectories(m_OOSLogPath, 0700); } file << "State hash: " << std::hex; std::string hashRaw; m_ComponentManager.ComputeStateHash(hashRaw, false); for (size_t i = 0; i < hashRaw.size(); ++i) file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; file << std::dec << "\n"; file << "\n"; m_ComponentManager.DumpDebugState(file, true); std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); } //////////////////////////////////////////////////////////////// CSimulation2::CSimulation2(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, cx, terrain)) { } CSimulation2::~CSimulation2() { delete m; } // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: void CSimulation2::EnableSerializationTest() { m->m_EnableSerializationTest = true; } void CSimulation2::EnableRejoinTest(int rejoinTestTurn) { m->m_RejoinTestTurn = rejoinTestTurn; } void CSimulation2::EnableOOSLog() { if (m->m_EnableOOSLog) return; m->m_EnableOOSLog = true; m->m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m->m_OOSLogPath.string8().c_str()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); } entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); } void CSimulation2::DestroyEntity(entity_id_t ent) { m->m_ComponentManager.DestroyComponentsSoon(ent); } void CSimulation2::FlushDestroyedEntities() { m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const { return m->m_ComponentManager.QueryInterface(ent, iid); } void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const { m->m_ComponentManager.PostMessage(ent, msg); } void CSimulation2::BroadcastMessage(const CMessage& msg) const { m->m_ComponentManager.BroadcastMessage(msg); } CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid) { return m->m_ComponentManager.GetEntitiesWithInterface(iid); } const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid) { return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid); } const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; } ScriptInterface& CSimulation2::GetScriptInterface() const { return m->m_ComponentManager.GetScriptInterface(); } void CSimulation2::PreInitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "PreInitGame"); } void CSimulation2::InitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue settings(rq.cx); JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes()); Script::GetProperty(rq, tmpInitAttributes, "settings", &settings); ScriptFunction::CallVoid(rq, global, "InitGame", settings); } void CSimulation2::Update(int turnLength) { std::vector commands; m->Update(turnLength, commands); } void CSimulation2::Update(int turnLength, const std::vector& commands) { m->Update(turnLength, commands); } void CSimulation2::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { m->Interpolate(simFrameLength, frameOffset, realFrameLength); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { PROFILE3("sim submit"); CMessageRenderSubmit msg(collector, frustum, culling); m->m_ComponentManager.BroadcastMessage(msg); } float CSimulation2::GetLastFrameOffset() const { return m->m_LastFrameOffset; } bool CSimulation2::LoadScripts(const VfsPath& path) { return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path); } bool CSimulation2::LoadDefaultScripts() { return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts); } void CSimulation2::SetStartupScript(const std::string& code) { m->m_StartupScript = code; } const std::string& CSimulation2::GetStartupScript() { return m->m_StartupScript; } void CSimulation2::SetInitAttributes(JS::HandleValue attribs) { m->m_InitAttributes = attribs; } JS::Value CSimulation2::GetInitAttributes() { return m->m_InitAttributes.get(); } void CSimulation2::GetInitAttributes(JS::MutableHandleValue ret) { ret.set(m->m_InitAttributes); } void CSimulation2::SetMapSettings(const std::string& settings) { Script::ParseJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), settings, &m->m_MapSettings); } void CSimulation2::SetMapSettings(JS::HandleValue settings) { m->m_MapSettings = settings; m->InitRNGSeedSimulation(); m->InitRNGSeedAI(); } std::string CSimulation2::GetMapSettingsString() { return Script::StringifyJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), &m->m_MapSettings); } void CSimulation2::GetMapSettings(JS::MutableHandleValue ret) { ret.set(m->m_MapSettings); } void CSimulation2::LoadPlayerSettings(bool newPlayers) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); } void CSimulation2::LoadMapSettings() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); // Initialize here instead of in Update() ScriptFunction::CallVoid(rq, global, "LoadMapSettings", m->m_MapSettings); Script::FreezeObject(rq, m->m_InitAttributes, true); GetScriptInterface().SetGlobal("InitAttributes", m->m_InitAttributes, true, true, true); if (!m->m_StartupScript.empty()) GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript); // Load the trigger scripts after we have loaded the simulation and the map. m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts); } int CSimulation2::ProgressiveLoad() { return m->ProgressiveLoad(); } Status CSimulation2::ReloadChangedFile(const VfsPath& path) { return m->ReloadChangedFile(path); } void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI) { m->ResetState(skipScriptedComponents, skipAI); } bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick) { return m->m_ComponentManager.ComputeStateHash(outHash, quick); } bool CSimulation2::DumpDebugState(std::ostream& stream) { stream << "sim turn: " << m->m_TurnNumber << std::endl; return m->m_ComponentManager.DumpDebugState(stream, true); } bool CSimulation2::SerializeState(std::ostream& stream) { return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } void CSimulation2::ActivateRejoinTest(int turn) { if (m->m_RejoinTestTurn != -1) return; LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber); m->m_RejoinTestTurn = turn; } std::string CSimulation2::GenerateSchema() { return m->m_ComponentManager.GenerateSchema(); } static std::vector GetJSONData(const VfsPath& path) { VfsPaths pathnames; Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames); if (ret != INFO::OK) { // Some error reading directory wchar_t error[200]; LOGERROR("Error reading directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return std::vector(); } std::vector data; for (const VfsPath& p : pathnames) { // Load JSON file CVFSFile file; PSRETURN loadStatus = file.Load(g_VFS, p); if (loadStatus != PSRETURN_OK) { LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(loadStatus)); continue; } data.push_back(file.DecodeUTF8()); // assume it's UTF-8 } return data; } std::vector CSimulation2::GetRMSData() { return GetJSONData(L"maps/random/"); } -std::vector CSimulation2::GetCivData() -{ - return GetJSONData(L"simulation/data/civs/"); -} - std::vector CSimulation2::GetVictoryConditiondData() { return GetJSONData(L"simulation/data/settings/victory_conditions/"); } static std::string ReadJSON(const VfsPath& path) { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return std::string(); } // Load JSON file CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return std::string(); } return file.DecodeUTF8(); // assume it's UTF-8 } std::string CSimulation2::GetPlayerDefaults() { return ReadJSON(L"simulation/data/settings/player_defaults.json"); } std::string CSimulation2::GetMapSizes() { return ReadJSON(L"simulation/data/settings/map_sizes.json"); } std::string CSimulation2::GetAIData() { const ScriptInterface& scriptInterface = GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue aiData(rq.cx, ICmpAIManager::GetAIs(scriptInterface)); // Build single JSON string with array of AI data JS::RootedValue ais(rq.cx); if (!Script::CreateObject(rq, &ais, "AIData", aiData)) return std::string(); return Script::StringifyJSON(rq, &ais); } Index: ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 26297) +++ ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 26298) @@ -1,238 +1,257 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedTypes.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/TemplateLoader.h" #include "ps/XML/RelaxNG.h" class CCmpTemplateManager : public ICmpTemplateManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(TemplateManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_DisableValidation = false; m_Validator.LoadGrammar(GetSimContext().GetComponentManager().GenerateSchema()); // TODO: handle errors loading the grammar here? // TODO: support hotloading changes to the grammar } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { std::map> templateMap; for (const std::pair& templateEnt : m_LatestTemplates) if (!ENTITY_IS_LOCAL(templateEnt.first)) templateMap[templateEnt.second].push_back(templateEnt.first); Serializer(serialize, "templates", templateMap); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); std::map> templateMap; Serializer(deserialize, "templates", templateMap); for (const std::pair>& mapEl : templateMap) for (entity_id_t id : mapEl.second) m_LatestTemplates[id] = mapEl.first; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); // Clean up m_LatestTemplates so it doesn't record any data for destroyed entities m_LatestTemplates.erase(msgData.entity); break; } } } virtual void DisableValidation() { m_DisableValidation = true; } virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::string& templateName); virtual const CParamNode* GetTemplate(const std::string& templateName); virtual const CParamNode* GetTemplateWithoutValidation(const std::string& templateName); virtual bool TemplateExists(const std::string& templateName) const; virtual const CParamNode* LoadLatestTemplate(entity_id_t ent); virtual std::string GetCurrentTemplateName(entity_id_t ent) const; virtual std::vector FindAllTemplates(bool includeActors) const; + virtual std::vector> GetCivData(); + virtual std::vector FindUsedTemplates() const; virtual std::vector GetEntitiesUsingTemplate(const std::string& templateName) const; private: // Template loader CTemplateLoader m_templateLoader; // Entity template XML validator RelaxNGValidator m_Validator; // Disable validation, for test cases bool m_DisableValidation; // Map from template name to schema validation status. // (Some files, e.g. inherited parent templates, may not be valid themselves but we still need to load // them and use them; we only reject invalid templates that were requested directly by GetTemplate/etc) std::map m_TemplateSchemaValidity; // Remember the template used by each entity, so we can return them // again for deserialization. std::map m_LatestTemplates; }; REGISTER_COMPONENT_TYPE(TemplateManager) const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std::string& templateName) { m_LatestTemplates[ent] = templateName; return GetTemplate(templateName); } const CParamNode* CCmpTemplateManager::GetTemplate(const std::string& templateName) { const CParamNode& fileData = m_templateLoader.GetTemplateFileData(templateName); if (!fileData.IsOk()) return NULL; if (!m_DisableValidation) { // Compute validity, if it's not computed before if (m_TemplateSchemaValidity.find(templateName) == m_TemplateSchemaValidity.end()) { m_TemplateSchemaValidity[templateName] = m_Validator.Validate(templateName, fileData.ToXMLString()); // Show error on the first failure to validate the template if (!m_TemplateSchemaValidity[templateName]) LOGERROR("Failed to validate entity template '%s'", templateName.c_str()); } // Refuse to return invalid templates if (!m_TemplateSchemaValidity[templateName]) return NULL; } const CParamNode& templateRoot = fileData.GetChild("Entity"); if (!templateRoot.IsOk()) { // The validator should never let this happen LOGERROR("Invalid root element in entity template '%s'", templateName.c_str()); return NULL; } return &templateRoot; } const CParamNode* CCmpTemplateManager::GetTemplateWithoutValidation(const std::string& templateName) { const CParamNode& templateRoot = m_templateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) return NULL; return &templateRoot; } bool CCmpTemplateManager::TemplateExists(const std::string& templateName) const { return m_templateLoader.TemplateExists(templateName); } const CParamNode* CCmpTemplateManager::LoadLatestTemplate(entity_id_t ent) { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return NULL; return LoadTemplate(ent, it->second); } std::string CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent) const { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return ""; return it->second; } std::vector CCmpTemplateManager::FindAllTemplates(bool includeActors) const { ETemplatesType templatesType = includeActors ? ALL_TEMPLATES : SIMULATION_TEMPLATES; return m_templateLoader.FindTemplates("", true, templatesType); } +std::vector> CCmpTemplateManager::GetCivData() +{ + std::vector> data; + + std::vector names = m_templateLoader.FindTemplatesUnrestricted("special/players/", false); + data.reserve(names.size()); + for (const std::string& name : names) + { + const CParamNode& identity = GetTemplate(name)->GetChild("Identity"); + data.push_back(std::vector { + identity.GetChild("Civ").ToWString(), + identity.GetChild("GenericName").ToWString() + }); + } + return data; +} + std::vector CCmpTemplateManager::FindUsedTemplates() const { std::vector usedTemplates; for (const std::pair& p : m_LatestTemplates) if (std::find(usedTemplates.begin(), usedTemplates.end(), p.second) == usedTemplates.end()) usedTemplates.push_back(p.second); return usedTemplates; } /** * Get the list of entities using the specified template */ std::vector CCmpTemplateManager::GetEntitiesUsingTemplate(const std::string& templateName) const { std::vector entities; for (const std::pair& p : m_LatestTemplates) if (p.second == templateName) entities.push_back(p.first); return entities; } Index: ps/trunk/source/simulation2/components/ICmpPlayer.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpPlayer.h (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpPlayer.h (revision 26298) @@ -1,46 +1,45 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPPLAYER #define INCLUDED_ICMPPLAYER #include "simulation2/system/Interface.h" struct CColor; class CFixedVector3D; /** * Player data. * (This interface includes the functions needed by native code for loading maps, * and for minimap rendering; most player interaction is handled by scripts instead. * Also includes some functions needed for the non visual autostart.) */ class ICmpPlayer : public IComponent { public: virtual CColor GetDisplayedColor() = 0; - virtual std::wstring GetCiv() = 0; virtual CFixedVector3D GetStartingCameraPos() = 0; virtual CFixedVector3D GetStartingCameraRot() = 0; virtual bool HasStartingCamera() = 0; virtual std::string GetState() = 0; DECLARE_INTERFACE_TYPE(Player) }; #endif // INCLUDED_ICMPPLAYER Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/PlayerHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/PlayerHandlers.cpp (revision 26297) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/PlayerHandlers.cpp (revision 26298) @@ -1,43 +1,44 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MessageHandler.h" #include "ps/Game.h" #include "simulation2/Simulation2.h" - +#include "simulation2/components/ICmpTemplateManager.h" namespace AtlasMessage { QUERYHANDLER(GetCivData) { - msg->data = g_Game->GetSimulation2()->GetCivData(); + CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); + msg->data = cmpTemplateManager->GetCivData(); } QUERYHANDLER(GetPlayerDefaults) { msg->defaults = g_Game->GetSimulation2()->GetPlayerDefaults(); } QUERYHANDLER(GetAIData) { msg->data = g_Game->GetSimulation2()->GetAIData(); } } Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/mace.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/mace.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/mace.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/mace_player_teambonus + + + mace + Macedonians + Macedonia was an ancient Greek kingdom, centered in the northeastern part of the Greek peninsula. Under the leadership of Alexander the Great, Macedonian forces and allies took over most of the world they knew, including Egypt, Persia and parts of the Indian subcontinent, allowing a diffusion of Hellenic and eastern cultures for years to come. + session/portraits/emblems/emblem_macedonians.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/mace.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/rome.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/rome.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/rome.xml (revision 26298) @@ -0,0 +1,12 @@ + + + + teambonuses/rome_player_teambonus + + + rome + Romans + The Romans controlled one of the largest empires of the ancient world, stretching at its peak from southern Scotland to the Sahara Desert, and containing between 60 million and 80 million inhabitants, one quarter of the Earth's population at that time. Rome also remained one of the strongest nations on earth for almost 800 years. The Romans were the supreme builders of the ancient world, excelled at siege warfare and had an exquisite infantry and navy. + session/portraits/emblems/emblem_romans.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/rome.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/source/ps/TemplateLoader.h =================================================================== --- ps/trunk/source/ps/TemplateLoader.h (revision 26297) +++ ps/trunk/source/ps/TemplateLoader.h (revision 26298) @@ -1,95 +1,101 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_TEMPLATELOADER #define INCLUDED_TEMPLATELOADER #include "simulation2/system/ParamNode.h" #include #include enum ETemplatesType { ALL_TEMPLATES, ACTOR_TEMPLATES, SIMULATION_TEMPLATES }; /** * Template loader: Handles the loading of entity template files for: * - the initialisation and deserialization of entity components in the * simulation (CmpTemplateManager). * - access to actor templates, obstruction data, etc. in RMS/RMGEN * - access to various templates in the GUI, to display faction specificities * * Template names are intentionally restricted to ASCII strings for storage/serialization * efficiency (we have a lot of strings so this is significant); * they correspond to filenames so they shouldn't contain non-ASCII anyway. * * * TODO: Find a way to validate templates outside of the simulation. */ class CTemplateLoader { public: CTemplateLoader() { } /** * Provides the file data for requested template. */ const CParamNode& GetTemplateFileData(const std::string& templateName); /** * Check if the template XML file exits, without trying to load it. */ bool TemplateExists(const std::string& templateName) const; /** * Returns a list of strings that could be validly passed as @c templateName to LoadTemplateFile. * (This includes "actor|foo" etc names). */ std::vector FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType) const; + /** + * Returns a list of strings that could validly be passed as @c templateName to LoadTemplateFile. + * Not ignoring any special directories. + */ + std::vector FindTemplatesUnrestricted(const std::string& path, bool includeSubdirectories) const; + private: /** * (Re)loads the given template, regardless of whether it exists already, * and saves into m_TemplateFileData. Also loads any parents that are not yet * loaded. Returns false on error. * @param templateName - XML filename to load (may be a |-separated string) * @param compositing - whether this template is an intermediary layer in a |-separated string. * @param depth - the current recursion depth. */ bool LoadTemplateFile(CParamNode& node, std::string_view templateName, bool compositing, int depth); /** * Constructs a standard static-decorative-object template for the given actor */ void ConstructTemplateActor(std::string_view actorName, CParamNode& out); /** * Map from template name (XML filename or special |-separated string) to the most recently * loaded non-broken template data. This includes files that will fail schema validation. * (Failed loads won't remove existing entries under the same name, so we behave more nicely * when hotloading broken files) */ std::unordered_map m_TemplateFileData; }; #endif // INCLUDED_TEMPLATELOADER Index: ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.h =================================================================== --- ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.h (revision 26297) +++ ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.h (revision 26298) @@ -1,274 +1,275 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_CCMPRALLYPOINTRENDERER #define INCLUDED_CCMPRALLYPOINTRENDERER #include "ICmpRallyPointRenderer.h" #include "graphics/Overlay.h" #include "graphics/TextureManager.h" #include "ps/CLogger.h" #include "renderer/Renderer.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFootprint.h" +#include "simulation2/components/ICmpIdentity.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/system/Component.h" struct SVisibilitySegment { bool m_Visible; size_t m_StartIndex; size_t m_EndIndex; // Inclusive SVisibilitySegment(bool visible, size_t startIndex, size_t endIndex) : m_Visible(visible), m_StartIndex(startIndex), m_EndIndex(endIndex) {} bool operator==(const SVisibilitySegment& other) const { return m_Visible == other.m_Visible && m_StartIndex == other.m_StartIndex && m_EndIndex == other.m_EndIndex; } bool operator!=(const SVisibilitySegment& other) const { return !(*this == other); } bool IsSinglePoint() const { return m_StartIndex == m_EndIndex; } }; class CCmpRallyPointRenderer : public ICmpRallyPointRenderer { public: static std::string GetSchema(); static void ClassInit(CComponentManager& componentManager); virtual void Init(const CParamNode& paramNode); virtual void Deinit(); virtual void Serialize(ISerializer& UNUSED(serialize)); virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)); virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)); /* * Must be called whenever m_Displayed or the size of m_RallyPoints change, * to determine whether we need to respond to render messages. */ virtual void UpdateMessageSubscriptions(); virtual void AddPosition_wrapper(const CFixedVector2D& pos); virtual void SetPosition(const CFixedVector2D& pos); virtual void UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos); virtual void SetDisplayed(bool displayed); virtual void Reset(); virtual void UpdateColor(); /** * Returns true if at least one display rally point is set; i.e., if we have a point to render our marker/line at. */ virtual bool IsSet() const; DEFAULT_COMPONENT_ALLOCATOR(RallyPointRenderer) protected: /** * Display position of the rally points. Note that this are merely the display positions; they not necessarily the same as the * actual positions used in the simulation at any given time. In particular, we need this separate copy to support * instantaneously rendering the rally point markers/lines when the user sets one in-game (instead of waiting until the * network-synchronization code sets it on the RallyPoint component, which might take up to half a second). */ std::vector m_RallyPoints; /** * Full path to the rally points as returned by the pathfinder, with some post-processing applied to reduce zig/zagging. */ std::vector > m_Path; /** * Visibility segments of the rally point paths; splits the path into SoD/non-SoD segments. */ std::vector > m_VisibilitySegments; /** * Should we render the rally points and the path lines? (set from JS when e.g. the unit is selected/deselected) */ bool m_Displayed; /** * Smooth the path before rendering? */ bool m_SmoothPath; /** * Entity IDs of the rally point markers. */ std::vector m_MarkerEntityIds; size_t m_LastMarkerCount; /** * Last seen owner of this entity (used to keep track of ownership changes). */ player_id_t m_LastOwner; /** * Template name of the rally point markers. */ std::wstring m_MarkerTemplate; /** * Marker connector line settings (loaded from XML) */ float m_LineThickness; CColor m_LineColor; CColor m_LineDashColor; SOverlayTexturedLine::LineCapType m_LineStartCapType; SOverlayTexturedLine::LineCapType m_LineEndCapType; std::wstring m_LineTexturePath; std::wstring m_LineTextureMaskPath; /** * Pathfinder passability class to use for computing the (long-range) marker line path. */ std::string m_LinePassabilityClass; CTexturePtr m_Texture; CTexturePtr m_TextureMask; /** * Textured overlay lines to be used for rendering the marker line. There can be multiple because we may need to render * dashes for segments that are inside the SoD. */ std::vector > m_TexturedOverlayLines; /** * Draw little overlay circles to indicate where the exact path points are. */ bool m_EnableDebugNodeOverlay; std::vector > m_DebugNodeOverlays; private: /** * Helper function for AddPosition_wrapper and SetPosition. */ void AddPosition(CFixedVector2D pos, bool recompute); /** * Helper function to set the line color to its owner's color. */ void UpdateLineColor(); /** * Repositions the rally point markers; moves them outside of the world (ie. hides them), or positions them at the currently * set rally points. Also updates the actor's variation according to the entity's current owning player's civilization. * * Should be called whenever either the position of a rally point changes (including whether it is set or not), or the display * flag changes, or the ownership of the entity changes. */ void UpdateMarkers(); /** * Recomputes all the full paths from this entity to the rally point and from the rally point to the next, and does all the necessary * post-processing to make them prettier. * * Should be called whenever all rally points' position changes. */ void RecomputeAllRallyPointPaths(); /** * Recomputes the full path for m_Path[ @p index], and does all the necessary post-processing to make it prettier. * * Should be called whenever either the starting position or the rally point's position changes. */ void RecomputeRallyPointPath_wrapper(size_t index); /** * Recomputes the full path from this entity/the previous rally point to the next rally point, and does all the necessary * post-processing to make it prettier. This doesn't check if we have a valid position or if a rally point is set. * * You shouldn't need to call this method directly. */ void RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder); /** * Checks for changes to the SoD to the previously saved state, and reconstructs the visibility segments and overlay lines to * match if necessary. Does nothing if the rally point lines are not currently set to be displayed, or if no rally point is set. */ void UpdateOverlayLines(); /** * Sets up all overlay lines for rendering according to the current full path and visibility segments. Splits the line into solid * and dashed pieces (for the SoD). Should be called whenever the SoD has changed. If no full path is currently set, this method * does nothing. */ void ConstructAllOverlayLines(); /** * Sets up the overlay lines for rendering according to the full path and visibility segments at @p index. Splits the line into * solid and dashed pieces (for the SoD). Should be called whenever the SoD of the path at @p index has changed. */ void ConstructOverlayLines(size_t index); /** * Get the point on the footprint edge that's as close from "start" as possible. */ void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; /** * Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from * building/previous rally point to rally point. Used to construct the overlay line segments and track changes to the SoD. */ void GetVisibilitySegments(std::vector& out, size_t index) const; /** * Simplifies the path by removing waypoints that lie between two points that are visible from one another. This is primarily * intended to reduce some unnecessary curviness of the path; the pathfinder returns a mathematically (near-)optimal path, which * will happily curve and bend to reduce costs. Visually, it doesn't make sense for a rally point path to curve and bend when it * could just as well have gone in a straight line; that's why we have this, to make it look more natural. * * @p coords array of path coordinates to simplify * @p maxSegmentLinks if non-zero, indicates the maximum amount of consecutive node-to-node links that can be joined into a * single link. If this value is set to e.g. 1, then no reductions will be performed. A value of 3 means that * at most 3 consecutive node links will be joined into a single link. * @p floating whether to consider nodes who are under the water level as floating on top of the water */ void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const; /** * Helper function to GetVisibilitySegments, factored out for testing. Merges single-point segments with its neighbouring * segments. You should not have to call this method directly. */ static void MergeVisibilitySegments(std::vector& segments); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); }; REGISTER_COMPONENT_TYPE(RallyPointRenderer) #endif // INCLUDED_CCMPRALLYPOINTRENDERER Index: ps/trunk/source/simulation2/components/ICmpPlayer.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpPlayer.cpp (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpPlayer.cpp (revision 26298) @@ -1,66 +1,61 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpPlayer.h" #include "graphics/Color.h" #include "maths/FixedVector3D.h" #include "simulation2/system/InterfaceScripted.h" #include "simulation2/scripting/ScriptComponent.h" BEGIN_INTERFACE_WRAPPER(Player) END_INTERFACE_WRAPPER(Player) class CCmpPlayerScripted : public ICmpPlayer { public: DEFAULT_SCRIPT_WRAPPER(PlayerScripted) virtual CColor GetDisplayedColor() { return m_Script.Call("GetDisplayedColor"); } - virtual std::wstring GetCiv() - { - return m_Script.Call("GetCiv"); - } - virtual CFixedVector3D GetStartingCameraPos() { return m_Script.Call("GetStartingCameraPos"); } virtual CFixedVector3D GetStartingCameraRot() { return m_Script.Call("GetStartingCameraRot"); } virtual bool HasStartingCamera() { return m_Script.Call("HasStartingCamera"); } virtual std::string GetState() { return m_Script.Call("GetState"); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(PlayerScripted) Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Player/Player.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Player/Player.cpp (revision 26297) +++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Player/Player.cpp (revision 26298) @@ -1,1002 +1,1001 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Player.h" #include "AtlasObject/AtlasObject.h" #include "CustomControls/ColorDialog/ColorDialog.h" #include "ScenarioEditor/ScenarioEditor.h" #include "wx/choicebk.h" enum { ID_NumPlayers, ID_PlayerFood, ID_PlayerWood, ID_PlayerMetal, ID_PlayerStone, ID_PlayerPop, ID_PlayerColor, ID_DefaultName, ID_DefaultCiv, ID_DefaultColor, ID_DefaultAI, ID_DefaultFood, ID_DefaultWood, ID_DefaultMetal, ID_DefaultStone, ID_DefaultPop, ID_DefaultTeam, ID_CameraSet, ID_CameraView, ID_CameraClear }; // TODO: Some of these helper things should be moved out of this file // and into shared locations // Helper function for adding tooltips static wxWindow* Tooltipped(wxWindow* window, const wxString& tip) { window->SetToolTip(tip); return window; } ////////////////////////////////////////////////////////////////////////// class DefaultCheckbox : public wxCheckBox { public: DefaultCheckbox(wxWindow* parent, wxWindowID id, wxWindow* control, bool initialValue = false) : wxCheckBox(parent, id, wxEmptyString), m_Control(control) { SetValue(initialValue); } virtual void SetValue(bool value) { m_Control->Enable(value); wxCheckBox::SetValue(value); } void OnChecked(wxCommandEvent& evt) { m_Control->Enable(evt.IsChecked()); evt.Skip(); } private: wxWindow* m_Control; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(DefaultCheckbox, wxCheckBox) EVT_CHECKBOX(wxID_ANY, DefaultCheckbox::OnChecked) END_EVENT_TABLE(); class PlayerNotebookPage : public wxPanel { public: PlayerNotebookPage(wxWindow* parent, const wxString& name, size_t playerID) : wxPanel(parent, wxID_ANY), m_Name(name), m_PlayerID(playerID) { m_Controls.page = this; Freeze(); wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); SetSizer(sizer); { ///////////////////////////////////////////////////////////////////////// // Player Info wxStaticBoxSizer* playerInfoSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Player info")); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(3, 5, 5); gridSizer->AddGrowableCol(2); wxTextCtrl* nameCtrl = new wxTextCtrl(this, wxID_ANY); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultName, nameCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Name")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(nameCtrl, wxSizerFlags(1).Expand().Align(wxALIGN_RIGHT)); m_Controls.name = nameCtrl; wxChoice* civChoice = new wxChoice(this, wxID_ANY); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultCiv, civChoice), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Civilisation")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(civChoice, wxSizerFlags(1).Expand().Align(wxALIGN_RIGHT)); m_Controls.civ = civChoice; wxButton* colorButton = new wxButton(this, ID_PlayerColor); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultColor, colorButton), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Color")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(colorButton, _("Set player color")), wxSizerFlags(1).Expand().Align(wxALIGN_RIGHT)); m_Controls.color = colorButton; wxChoice* aiChoice = new wxChoice(this, wxID_ANY); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultAI, aiChoice), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("AI")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(aiChoice, _("Select AI")), wxSizerFlags(1).Expand().Align(wxALIGN_RIGHT)); m_Controls.ai = aiChoice; playerInfoSizer->Add(gridSizer, wxSizerFlags(1).Expand()); sizer->Add(playerInfoSizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } { ///////////////////////////////////////////////////////////////////////// // Resources wxStaticBoxSizer* resourceSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Resources")); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(3, 5, 5); gridSizer->AddGrowableCol(2); wxSpinCtrl* foodCtrl = new wxSpinCtrl(this, ID_PlayerFood, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, INT_MAX); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultFood, foodCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Food")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(foodCtrl, _("Initial value of food resource")), wxSizerFlags().Expand()); m_Controls.food = foodCtrl; wxSpinCtrl* woodCtrl = new wxSpinCtrl(this, ID_PlayerWood, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, INT_MAX); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultWood, woodCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Wood")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(woodCtrl, _("Initial value of wood resource")), wxSizerFlags().Expand()); m_Controls.wood = woodCtrl; wxSpinCtrl* metalCtrl = new wxSpinCtrl(this, ID_PlayerMetal, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, INT_MAX); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultMetal, metalCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Metal")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(metalCtrl, _("Initial value of metal resource")), wxSizerFlags().Expand()); m_Controls.metal = metalCtrl; wxSpinCtrl* stoneCtrl = new wxSpinCtrl(this, ID_PlayerStone, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, INT_MAX); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultStone, stoneCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Stone")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(stoneCtrl, _("Initial value of stone resource")), wxSizerFlags().Expand()); m_Controls.stone = stoneCtrl; wxSpinCtrl* popCtrl = new wxSpinCtrl(this, ID_PlayerPop, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, INT_MAX); gridSizer->Add(new DefaultCheckbox(this, ID_DefaultPop, popCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Pop limit")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(popCtrl, _("Population limit for this player")), wxSizerFlags().Expand()); m_Controls.pop = popCtrl; resourceSizer->Add(gridSizer, wxSizerFlags(1).Expand()); sizer->Add(resourceSizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } { ///////////////////////////////////////////////////////////////////////// // Diplomacy wxStaticBoxSizer* diplomacySizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Diplomacy")); wxBoxSizer* boxSizer = new wxBoxSizer(wxHORIZONTAL); wxChoice* teamCtrl = new wxChoice(this, wxID_ANY); boxSizer->Add(new DefaultCheckbox(this, ID_DefaultTeam, teamCtrl), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); boxSizer->AddSpacer(5); boxSizer->Add(new wxStaticText(this, wxID_ANY, _("Team")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); boxSizer->AddSpacer(5); teamCtrl->Append(_("None")); teamCtrl->Append(_T("1")); teamCtrl->Append(_T("2")); teamCtrl->Append(_T("3")); teamCtrl->Append(_T("4")); boxSizer->Add(teamCtrl); m_Controls.team = teamCtrl; diplomacySizer->Add(boxSizer, wxSizerFlags(1).Expand()); // TODO: possibly have advanced panel where each player's diplomacy can be set? // Advanced panel /*wxCollapsiblePane* advPane = new wxCollapsiblePane(this, wxID_ANY, _("Advanced")); wxWindow* pane = advPane->GetPane(); diplomacySizer->Add(advPane, 0, wxGROW | wxALL, 2);*/ sizer->Add(diplomacySizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } { ///////////////////////////////////////////////////////////////////////// // Camera wxStaticBoxSizer* cameraSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Starting Camera")); wxGridSizer* gridSizer = new wxGridSizer(3); wxButton* cameraSet = new wxButton(this, ID_CameraSet, _("Set"), wxDefaultPosition, wxSize(48, -1)); gridSizer->Add(Tooltipped(cameraSet, _("Set player camera to this view")), wxSizerFlags().Expand()); wxButton* cameraView = new wxButton(this, ID_CameraView, _("View"), wxDefaultPosition, wxSize(48, -1)); cameraView->Enable(false); gridSizer->Add(Tooltipped(cameraView, _("View the player camera")), wxSizerFlags().Expand()); wxButton* cameraClear = new wxButton(this, ID_CameraClear, _("Clear"), wxDefaultPosition, wxSize(48, -1)); cameraClear->Enable(false); gridSizer->Add(Tooltipped(cameraClear, _("Clear player camera")), wxSizerFlags().Expand()); cameraSizer->Add(gridSizer, wxSizerFlags().Expand()); sizer->Add(cameraSizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } Layout(); Thaw(); } void OnDisplay() { } PlayerPageControls GetControls() { return m_Controls; } wxString GetPlayerName() { return m_Name; } size_t GetPlayerID() { return m_PlayerID; } bool IsCameraDefined() { return m_CameraDefined; } sCameraInfo GetCamera() { return m_Camera; } void SetCamera(sCameraInfo info, bool isDefined = true) { m_Camera = info; m_CameraDefined = isDefined; // Enable/disable controls wxDynamicCast(FindWindow(ID_CameraView), wxButton)->Enable(isDefined); wxDynamicCast(FindWindow(ID_CameraClear), wxButton)->Enable(isDefined); } private: void OnColor(wxCommandEvent& evt) { // Show color dialog ColorDialog colorDlg(this, _T("Scenario Editor/PlayerColor"), m_Controls.color->GetBackgroundColour()); if (colorDlg.ShowModal() == wxID_OK) { m_Controls.color->SetBackgroundColour(colorDlg.GetColourData().GetColour()); // Pass event on to next handler evt.Skip(); } } void OnCameraSet(wxCommandEvent& evt) { AtlasMessage::qGetView qryView; qryView.Post(); SetCamera(qryView.info, true); // Pass event on to next handler evt.Skip(); } void OnCameraView(wxCommandEvent& WXUNUSED(evt)) { POST_MESSAGE(SetView, (m_Camera)); } void OnCameraClear(wxCommandEvent& evt) { SetCamera(sCameraInfo(), false); // Pass event on to next handler evt.Skip(); } sCameraInfo m_Camera; bool m_CameraDefined; wxString m_Name; size_t m_PlayerID; PlayerPageControls m_Controls; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(PlayerNotebookPage, wxPanel) EVT_BUTTON(ID_PlayerColor, PlayerNotebookPage::OnColor) EVT_BUTTON(ID_CameraSet, PlayerNotebookPage::OnCameraSet) EVT_BUTTON(ID_CameraView, PlayerNotebookPage::OnCameraView) EVT_BUTTON(ID_CameraClear, PlayerNotebookPage::OnCameraClear) END_EVENT_TABLE(); ////////////////////////////////////////////////////////////////////////// class PlayerNotebook : public wxChoicebook { public: PlayerNotebook(wxWindow *parent) : wxChoicebook(parent, wxID_ANY/*, wxDefaultPosition, wxDefaultSize, wxNB_FIXEDWIDTH*/) { } PlayerPageControls AddPlayer(wxString name, size_t player) { PlayerNotebookPage* playerPage = new PlayerNotebookPage(this, name, player); AddPage(playerPage, name); m_Pages.push_back(playerPage); return playerPage->GetControls(); } void ResizePlayers(size_t numPlayers) { wxASSERT(numPlayers <= m_Pages.size()); // We don't really want to destroy the windows corresponding // to the tabs, so we've kept them in a vector and will // only remove and add them to the notebook as needed int selection = GetSelection(); size_t pageCount = GetPageCount(); if (numPlayers > pageCount) { // Add previously removed pages for (size_t i = pageCount; i < numPlayers; ++i) { AddPage(m_Pages[i], m_Pages[i]->GetPlayerName()); } } else { // Remove previously added pages // we have to manually hide them or they remain visible for (size_t i = pageCount - 1; i >= numPlayers; --i) { m_Pages[i]->Hide(); RemovePage(i); } } // Workaround for bug on wxGTK 2.8: wxChoice selection doesn't update // (in fact it loses its selection when adding/removing pages) GetChoiceCtrl()->SetSelection(selection); } protected: void OnPageChanged(wxChoicebookEvent& evt) { if (evt.GetSelection() >= 0 && evt.GetSelection() < (int)GetPageCount()) { static_cast(GetPage(evt.GetSelection()))->OnDisplay(); } evt.Skip(); } private: std::vector m_Pages; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(PlayerNotebook, wxChoicebook) EVT_CHOICEBOOK_PAGE_CHANGED(wxID_ANY, PlayerNotebook::OnPageChanged) END_EVENT_TABLE(); ////////////////////////////////////////////////////////////////////////// class PlayerSettingsControl : public wxPanel { public: PlayerSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor); void CreateWidgets(); void LoadDefaults(); void ReadFromEngine(); AtObj UpdateSettingsObject(); private: void SendToEngine(); void OnEdit(wxCommandEvent& WXUNUSED(evt)) { if (!m_InGUIUpdate) { SendToEngine(); } } void OnEditSpin(wxSpinEvent& WXUNUSED(evt)) { if (!m_InGUIUpdate) { SendToEngine(); } } void OnPlayerColor(wxCommandEvent& WXUNUSED(evt)) { if (!m_InGUIUpdate) { SendToEngine(); // Update player settings, to show new color POST_MESSAGE(LoadPlayerSettings, (false)); } } void OnNumPlayersText(wxCommandEvent& WXUNUSED(evt)) { // Ignore because it will also trigger EVT_SPINCTRL // and we don't want to handle the same event twice } void OnNumPlayersSpin(wxSpinEvent& evt) { if (!m_InGUIUpdate) { wxASSERT(evt.GetInt() > 0); // When wxMessageBox pops up, wxSpinCtrl loses focus, which // forces another EVT_SPINCTRL event, which we don't want // to handle, so we check here for a change if (evt.GetInt() == (int)m_NumPlayers) { return; // No change } size_t oldNumPlayers = m_NumPlayers; m_NumPlayers = evt.GetInt(); if (m_NumPlayers < oldNumPlayers) { // Remove players, but check if they own any entities bool notified = false; for (size_t i = oldNumPlayers; i > m_NumPlayers; --i) { qGetPlayerObjects objectsQry(i); objectsQry.Post(); std::vector ids = *objectsQry.ids; if (ids.size() > 0) { if (!notified) { // TODO: Add option to reassign objects? if (wxMessageBox(_("WARNING: All objects belonging to the removed players will be deleted. Continue anyway?"), _("Remove player confirmation"), wxICON_EXCLAMATION | wxYES_NO) != wxYES) { // Restore previous player count m_NumPlayers = oldNumPlayers; wxDynamicCast(FindWindow(ID_NumPlayers), wxSpinCtrl)->SetValue(m_NumPlayers); return; } notified = true; } // Delete objects // TODO: Merge multiple commands? POST_COMMAND(DeleteObjects, (ids)); } } } m_Players->ResizePlayers(m_NumPlayers); SendToEngine(); // Reload players, notify observers POST_MESSAGE(LoadPlayerSettings, (true)); m_MapSettings.NotifyObservers(); } } // TODO: we shouldn't hardcode this, but instead dynamically create // new player notebook pages on demand; of course the default data // will be limited by the entries in player_defaults.json static const size_t MAX_NUM_PLAYERS = 8; bool m_InGUIUpdate; AtObj m_PlayerDefaults; PlayerNotebook* m_Players; std::vector m_PlayerControls; Observable& m_MapSettings; size_t m_NumPlayers; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(PlayerSettingsControl, wxPanel) EVT_BUTTON(ID_PlayerColor, PlayerSettingsControl::OnPlayerColor) EVT_BUTTON(ID_CameraSet, PlayerSettingsControl::OnEdit) EVT_BUTTON(ID_CameraClear, PlayerSettingsControl::OnEdit) EVT_CHECKBOX(wxID_ANY, PlayerSettingsControl::OnEdit) EVT_CHOICE(wxID_ANY, PlayerSettingsControl::OnEdit) EVT_TEXT(ID_NumPlayers, PlayerSettingsControl::OnNumPlayersText) EVT_TEXT(wxID_ANY, PlayerSettingsControl::OnEdit) EVT_SPINCTRL(ID_NumPlayers, PlayerSettingsControl::OnNumPlayersSpin) EVT_SPINCTRL(ID_PlayerFood, PlayerSettingsControl::OnEditSpin) EVT_SPINCTRL(ID_PlayerWood, PlayerSettingsControl::OnEditSpin) EVT_SPINCTRL(ID_PlayerMetal, PlayerSettingsControl::OnEditSpin) EVT_SPINCTRL(ID_PlayerStone, PlayerSettingsControl::OnEditSpin) EVT_SPINCTRL(ID_PlayerPop, PlayerSettingsControl::OnEditSpin) END_EVENT_TABLE(); PlayerSettingsControl::PlayerSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor) : wxPanel(parent, wxID_ANY), m_InGUIUpdate(false), m_MapSettings(scenarioEditor.GetMapSettings()), m_NumPlayers(0) { // To prevent recursion, don't handle GUI events right now m_InGUIUpdate = true; wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Player settings")); SetSizer(sizer); wxBoxSizer* boxSizer = new wxBoxSizer(wxHORIZONTAL); boxSizer->Add(new wxStaticText(this, wxID_ANY, _("Num players")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); wxSpinCtrl* numPlayersSpin = new wxSpinCtrl(this, ID_NumPlayers, wxEmptyString, wxDefaultPosition, wxSize(40, -1)); numPlayersSpin->SetValue(MAX_NUM_PLAYERS); numPlayersSpin->SetRange(1, MAX_NUM_PLAYERS); boxSizer->Add(numPlayersSpin); sizer->Add(boxSizer, wxSizerFlags().Expand().Proportion(0)); sizer->AddSpacer(5); m_Players = new PlayerNotebook(this); sizer->Add(m_Players, wxSizerFlags().Expand().Proportion(1)); m_InGUIUpdate = false; } void PlayerSettingsControl::CreateWidgets() { // To prevent recursion, don't handle GUI events right now m_InGUIUpdate = true; // Load default civ and player data wxArrayString civNames; wxArrayString civCodes; AtlasMessage::qGetCivData qryCiv; qryCiv.Post(); - std::vector civData = *qryCiv.data; - for (size_t i = 0; i < civData.size(); ++i) + std::vector> civData = *qryCiv.data; + for (const std::vector& civ : civData) { - AtObj civ = AtlasObject::LoadFromJSON(civData[i]); - civNames.Add(wxString::FromUTF8(civ["Name"])); - civCodes.Add(wxString::FromUTF8(civ["Code"])); + civCodes.Add(civ[0]); + civNames.Add(civ[1]); } // Load AI data ArrayOfAIData ais(AIData::CompareAIData); AtlasMessage::qGetAIData qryAI; qryAI.Post(); AtObj aiData = AtlasObject::LoadFromJSON(*qryAI.data); for (AtIter a = aiData["AIData"]["item"]; a.defined(); ++a) { ais.Add(new AIData(wxString::FromUTF8(a["id"]), wxString::FromUTF8(a["data"]["name"]))); } // Create player pages AtIter playerDefs = m_PlayerDefaults["item"]; if (playerDefs.defined()) ++playerDefs; // Skip gaia for (size_t i = 0; i < MAX_NUM_PLAYERS; ++i) { // Create new player tab and get controls wxString name(_("Unknown")); if (playerDefs["Name"].defined()) name = playerDefs["Name"]; PlayerPageControls controls = m_Players->AddPlayer(name, i); m_PlayerControls.push_back(controls); // Populate civ choice box wxChoice* civChoice = controls.civ; for (size_t j = 0; j < civNames.Count(); ++j) civChoice->Append(civNames[j], new wxStringClientData(civCodes[j])); civChoice->SetSelection(0); // Populate ai choice box wxChoice* aiChoice = controls.ai; aiChoice->Append(_(""), new wxStringClientData()); for (size_t j = 0; j < ais.Count(); ++j) aiChoice->Append(ais[j]->GetName(), new wxStringClientData(ais[j]->GetID())); aiChoice->SetSelection(0); if (playerDefs.defined()) ++playerDefs; } m_InGUIUpdate = false; } void PlayerSettingsControl::LoadDefaults() { AtlasMessage::qGetPlayerDefaults qryPlayers; qryPlayers.Post(); AtObj playerData = AtlasObject::LoadFromJSON(*qryPlayers.defaults); m_PlayerDefaults = *playerData["PlayerData"]; } void PlayerSettingsControl::ReadFromEngine() { AtlasMessage::qGetMapSettings qry; qry.Post(); if (!(*qry.settings).empty()) { // Prevent error if there's no map settings to parse m_MapSettings = AtlasObject::LoadFromJSON(*qry.settings); } else { // Use blank object, it will be created next m_MapSettings = AtObj(); } AtIter player = m_MapSettings["PlayerData"]["item"]; if (!m_MapSettings.defined() || !player.defined() || player.count() == 0) { // Player data missing - set number of players to max m_NumPlayers = MAX_NUM_PLAYERS; } else { ++player; // skip gaia m_NumPlayers = player.count(); } wxASSERT(m_NumPlayers <= MAX_NUM_PLAYERS && m_NumPlayers != 0); // To prevent recursion, don't handle GUI events right now m_InGUIUpdate = true; wxDynamicCast(FindWindow(ID_NumPlayers), wxSpinCtrl)->SetValue(m_NumPlayers); // Remove / add extra player pages as needed m_Players->ResizePlayers(m_NumPlayers); // Update player controls with player data AtIter playerDefs = m_PlayerDefaults["item"]; if (playerDefs.defined()) ++playerDefs; // skip gaia for (size_t i = 0; i < MAX_NUM_PLAYERS; ++i) { const PlayerPageControls& controls = m_PlayerControls[i]; // name wxString name(_("Unknown")); bool defined = player["Name"].defined(); if (defined) name = wxString::FromUTF8(player["Name"]); else if (playerDefs["Name"].defined()) name = wxString::FromUTF8(playerDefs["Name"]); controls.name->SetValue(name); wxDynamicCast(FindWindowById(ID_DefaultName, controls.page), DefaultCheckbox)->SetValue(defined); // civ wxChoice* choice = controls.civ; defined = player["Civ"].defined(); wxString civCode = wxString::FromUTF8(defined ? player["Civ"] : playerDefs["Civ"]); for (size_t j = 0; j < choice->GetCount(); ++j) { wxStringClientData* str = dynamic_cast(choice->GetClientObject(j)); if (str->GetData() == civCode) { choice->SetSelection(j); break; } } wxDynamicCast(FindWindowById(ID_DefaultCiv, controls.page), DefaultCheckbox)->SetValue(defined); // color wxColor color; AtObj clrObj = *player["Color"]; defined = clrObj.defined(); if (!defined) clrObj = *playerDefs["Color"]; color = wxColor((*clrObj["r"]).getInt(), (*clrObj["g"]).getInt(), (*clrObj["b"]).getInt()); controls.color->SetBackgroundColour(color); wxDynamicCast(FindWindowById(ID_DefaultColor, controls.page), DefaultCheckbox)->SetValue(defined); // player type defined = player["AI"].defined(); wxString aiID = wxString::FromUTF8(defined ? player["AI"] : playerDefs["AI"]); choice = controls.ai; if (!aiID.empty()) { // AI for (size_t j = 0; j < choice->GetCount(); ++j) { wxStringClientData* str = dynamic_cast(choice->GetClientObject(j)); if (str->GetData() == aiID) { choice->SetSelection(j); break; } } } else // Human choice->SetSelection(0); wxDynamicCast(FindWindowById(ID_DefaultAI, controls.page), DefaultCheckbox)->SetValue(defined); // resources AtObj resObj = *player["Resources"]; defined = resObj.defined() && resObj["food"].defined(); if (defined) controls.food->SetValue((*resObj["food"]).getInt()); else controls.food->SetValue(0); wxDynamicCast(FindWindowById(ID_DefaultFood, controls.page), DefaultCheckbox)->SetValue(defined); defined = resObj.defined() && resObj["wood"].defined(); if (defined) controls.wood->SetValue((*resObj["wood"]).getInt()); else controls.wood->SetValue(0); wxDynamicCast(FindWindowById(ID_DefaultWood, controls.page), DefaultCheckbox)->SetValue(defined); defined = resObj.defined() && resObj["metal"].defined(); if (defined) controls.metal->SetValue((*resObj["metal"]).getInt()); else controls.metal->SetValue(0); wxDynamicCast(FindWindowById(ID_DefaultMetal, controls.page), DefaultCheckbox)->SetValue(defined); defined = resObj.defined() && resObj["stone"].defined(); if (defined) controls.stone->SetValue((*resObj["stone"]).getInt()); else controls.stone->SetValue(0); wxDynamicCast(FindWindowById(ID_DefaultStone, controls.page), DefaultCheckbox)->SetValue(defined); // population limit defined = player["PopulationLimit"].defined(); if (defined) controls.pop->SetValue((*player["PopulationLimit"]).getInt()); else controls.pop->SetValue(0); wxDynamicCast(FindWindowById(ID_DefaultPop, controls.page), DefaultCheckbox)->SetValue(defined); // team defined = player["Team"].defined(); if (defined) controls.team->SetSelection((*player["Team"]).getInt() + 1); else controls.team->SetSelection(0); wxDynamicCast(FindWindowById(ID_DefaultTeam, controls.page), DefaultCheckbox)->SetValue(defined); // camera if (player["StartingCamera"].defined()) { sCameraInfo info; // Don't use wxAtof because it depends on locales which // may cause problems with decimal points // see: http://www.wxwidgets.org/docs/faqgtk.htm#locale AtObj camPos = *player["StartingCamera"]["Position"]; info.pX = (float)(*camPos["x"]).getDouble(); info.pY = (float)(*camPos["y"]).getDouble(); info.pZ = (float)(*camPos["z"]).getDouble(); AtObj camRot = *player["StartingCamera"]["Rotation"]; info.rX = (float)(*camRot["x"]).getDouble(); info.rY = (float)(*camRot["y"]).getDouble(); info.rZ = (float)(*camRot["z"]).getDouble(); controls.page->SetCamera(info, true); } else controls.page->SetCamera(sCameraInfo(), false); if (player.defined()) ++player; if (playerDefs.defined()) ++playerDefs; } // Send default properties to engine, since they might not be set SendToEngine(); m_InGUIUpdate = false; } AtObj PlayerSettingsControl::UpdateSettingsObject() { // Update player data in the map settings AtObj players; players.set("@array", ""); wxASSERT(m_NumPlayers <= MAX_NUM_PLAYERS); AtIter playerDefs = m_PlayerDefaults["item"]; if (playerDefs.defined()) ++playerDefs; // Skip gaia for (size_t i = 0; i < m_NumPlayers; ++i) { PlayerPageControls controls = m_PlayerControls[i]; AtObj player; // name wxTextCtrl* text = controls.name; if (text->IsEnabled()) player.set("Name", text->GetValue().utf8_str()); // civ wxChoice* choice = controls.civ; if (choice->IsEnabled() && choice->GetSelection() >= 0) { wxStringClientData* str = dynamic_cast(choice->GetClientObject(choice->GetSelection())); player.set("Civ", str->GetData().utf8_str()); } else player.unset("Civ"); // color if (controls.color->IsEnabled()) { wxColor color = controls.color->GetBackgroundColour(); AtObj clrObj; clrObj.setInt("r", (int)color.Red()); clrObj.setInt("g", (int)color.Green()); clrObj.setInt("b", (int)color.Blue()); player.set("Color", clrObj); } // player type choice = controls.ai; if (choice->IsEnabled() && choice->GetSelection() > 0) { // ai - get id wxStringClientData* str = dynamic_cast(choice->GetClientObject(choice->GetSelection())); player.set("AI", str->GetData().utf8_str()); } else // human player.unset("AI"); // resources AtObj resObj; if (controls.food->IsEnabled()) resObj.setInt("food", controls.food->GetValue()); if (controls.wood->IsEnabled()) resObj.setInt("wood", controls.wood->GetValue()); if (controls.metal->IsEnabled()) resObj.setInt("metal", controls.metal->GetValue()); if (controls.stone->IsEnabled()) resObj.setInt("stone", controls.stone->GetValue()); if (resObj.defined()) player.set("Resources", resObj); // population limit if (controls.pop->IsEnabled()) player.setInt("PopulationLimit", controls.pop->GetValue()); // team choice = controls.team; if (choice->IsEnabled() && choice->GetSelection() >= 0) player.setInt("Team", choice->GetSelection() - 1); // camera AtObj camObj; if (controls.page->IsCameraDefined()) { sCameraInfo cam = controls.page->GetCamera(); AtObj camPos; camPos.setDouble("x", cam.pX); camPos.setDouble("y", cam.pY); camPos.setDouble("z", cam.pZ); camObj.set("Position", camPos); AtObj camRot; camRot.setDouble("x", cam.rX); camRot.setDouble("y", cam.rY); camRot.setDouble("z", cam.rZ); camObj.set("Rotation", camRot); } player.set("StartingCamera", camObj); players.add("item", player); if (playerDefs.defined()) ++playerDefs; } m_MapSettings.set("PlayerData", players); return m_MapSettings; } void PlayerSettingsControl::SendToEngine() { UpdateSettingsObject(); std::string json = AtlasObject::SaveToJSON(m_MapSettings); // TODO: would be nice if we supported undo for settings changes POST_COMMAND(SetMapSettings, (json)); } ////////////////////////////////////////////////////////////////////////// PlayerSidebar::PlayerSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer) : Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer), m_Loaded(false) { wxSizer* scrollSizer = new wxBoxSizer(wxVERTICAL); wxScrolledWindow* scrolledWindow = new wxScrolledWindow(this); scrolledWindow->SetScrollRate(10, 10); scrolledWindow->SetSizer(scrollSizer); m_MainSizer->Add(scrolledWindow, wxSizerFlags().Proportion(1).Expand()); m_PlayerSettingsCtrl = new PlayerSettingsControl(scrolledWindow, m_ScenarioEditor); scrollSizer->Add(m_PlayerSettingsCtrl, wxSizerFlags().Expand()); } void PlayerSidebar::OnCollapse(wxCollapsiblePaneEvent& WXUNUSED(evt)) { Freeze(); // Toggling the collapsing doesn't seem to update the sidebar layout // automatically, so do it explicitly here Layout(); Refresh(); // fixes repaint glitch on Windows Thaw(); } void PlayerSidebar::OnFirstDisplay() { // We do this here becase messages are used which requires simulation to be init'd m_PlayerSettingsCtrl->LoadDefaults(); m_PlayerSettingsCtrl->CreateWidgets(); m_PlayerSettingsCtrl->ReadFromEngine(); m_Loaded = true; Layout(); } void PlayerSidebar::OnMapReload() { // Make sure we've loaded the controls if (m_Loaded) { m_PlayerSettingsCtrl->ReadFromEngine(); } } BEGIN_EVENT_TABLE(PlayerSidebar, Sidebar) EVT_COLLAPSIBLEPANE_CHANGED(wxID_ANY, PlayerSidebar::OnCollapse) END_EVENT_TABLE(); Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/ptol.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/ptol.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/ptol.xml (revision 26298) @@ -0,0 +1,20 @@ + + + + teambonuses/ptol_player_teambonus + + + + + phase_town + Hero + + + + + ptol + Ptolemies + The Ptolemaic dynasty was a Macedonian Greek royal family which ruled the Ptolemaic Empire in Egypt during the Hellenistic period. Their rule lasted for 275 years, from 305 BC to 30 BC. They were the last dynasty of ancient Egypt. + session/portraits/emblems/emblem_ptolemies.png + + Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/ptol.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/xml \ No newline at end of property Index: ps/trunk/source/ps/TemplateLoader.cpp =================================================================== --- ps/trunk/source/ps/TemplateLoader.cpp (revision 26297) +++ ps/trunk/source/ps/TemplateLoader.cpp (revision 26298) @@ -1,180 +1,206 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "TemplateLoader.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/"; static const wchar_t ACTOR_ROOT[] = L"art/actors/"; static CParamNode NULL_NODE(false); bool CTemplateLoader::LoadTemplateFile(CParamNode& node, std::string_view templateName, bool compositing, int depth) { // Handle special case "actor|foo", which does not load 'foo' at all, just uses the name. if (templateName.compare(0, 6, "actor|") == 0) { ConstructTemplateActor(templateName.substr(6), node); return true; } // Handle infinite loops more gracefully than running out of stack space and crashing if (depth > 100) { LOGERROR("Probable infinite inheritance loop in entity template '%s'", std::string(templateName)); return false; } size_t pos = templateName.find_first_of('|'); if (pos != std::string::npos) { // 'foo|bar' pattern: 'bar' is treated as the parent of 'foo'. if (!LoadTemplateFile(node, templateName.substr(pos + 1), false, depth + 1)) return false; if (!LoadTemplateFile(node, templateName.substr(0, pos), true, depth + 1)) return false; return true; } // Load the data we need to apply on the node. This data may contain special modifiers, // such as filters, merges, multiplying the parent values, etc. Applying it to paramnode is destructive. // Find the XML file to load - by default, this assumes the files reside in 'special/filter'. // If not found there, it will be searched for in 'mixins/', then from the root. // The reason for this order is that filters are used at runtime, mixins at load time. std::wstring wtempName = wstring_from_utf8(std::string(templateName) + ".xml"); VfsPath path = VfsPath(TEMPLATE_ROOT) / L"special" / L"filter" / wtempName; if (!VfsFileExists(path)) path = VfsPath(TEMPLATE_ROOT) / L"mixins" / wtempName; if (!VfsFileExists(path)) path = VfsPath(TEMPLATE_ROOT) / wtempName; CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path); if (ok != PSRETURN_OK) return false; // (Xeromyces already logged an error with the full filename) // If the layer defines an explicit parent, we must load that and apply it before ourselves. int attr_parent = xero.GetAttributeID("parent"); CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent); if (!parentName.empty() && !LoadTemplateFile(node, parentName, compositing, depth + 1)) return false; // Load the new file into the template data (overriding parent values). // TODO: error handling. CParamNode::LoadXML(node, xero); return true; } static Status AddToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the .xml extension VfsPath pathstem = pathname.ChangeExtension(L""); // Strip the root from the path std::string name = pathstem.string8().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); // We want to ignore template_*.xml templates, since they should never be built in the editor if (name.substr(0, 9) == "template_") return INFO::OK; // Also ignore some subfolders. if (name.substr(0, 8) == "special/" || name.substr(0, 7) == "mixins/") return INFO::OK; templates.push_back(name); return INFO::OK; } +static Status AddToTemplatesUnrestricted(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) +{ + std::vector& templates = *(std::vector*)cbData; + + VfsPath pathstem = pathname.ChangeExtension(L""); + std::string name = pathstem.string8().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); + + // We want to ignore template_*.xml templates, since they may be incomplete. + if (name.substr(0, 9) == "template_") + return INFO::OK; + + templates.push_back(name); + return INFO::OK; +} + static Status AddActorToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the root from the path std::wstring name = pathname.string().substr(ARRAY_SIZE(ACTOR_ROOT)-1); templates.push_back("actor|" + std::string(name.begin(), name.end())); return INFO::OK; } bool CTemplateLoader::TemplateExists(const std::string& templateName) const { size_t pos = templateName.rfind('|'); std::string baseName(pos != std::string::npos ? templateName.substr(pos+1) : templateName); return VfsFileExists(VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(baseName + ".xml")); } std::vector CTemplateLoader::FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType) const { std::vector templates; if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES) { LOGERROR("Undefined template type (valid: all, simulation, actor)"); return templates; } size_t flags = includeSubdirectories ? vfs::DIR_RECURSIVE : 0; if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES) WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(TEMPLATE_ROOT) / path, AddToTemplates, (uintptr_t)&templates, L"*.xml", flags)); if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES) WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(ACTOR_ROOT) / path, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", flags)); return templates; } +std::vector CTemplateLoader::FindTemplatesUnrestricted(const std::string& path, bool includeSubdirectories) const +{ + std::vector templates; + + size_t flags = includeSubdirectories ? vfs::DIR_RECURSIVE : 0; + + WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(TEMPLATE_ROOT) / path, AddToTemplatesUnrestricted, (uintptr_t)&templates, L"*.xml", flags)); + + return templates; +} + const CParamNode& CTemplateLoader::GetTemplateFileData(const std::string& templateName) { if (std::unordered_map::const_iterator it = m_TemplateFileData.find(templateName); it != m_TemplateFileData.end()) return it->second; CParamNode ret; if (!LoadTemplateFile(ret, templateName, false, 0)) { LOGERROR("Failed to load entity template '%s'", templateName.c_str()); return NULL_NODE; } return m_TemplateFileData.insert_or_assign(templateName, ret).first->second; } void CTemplateLoader::ConstructTemplateActor(std::string_view actorName, CParamNode& out) { // Copy the actor template out = GetTemplateFileData("special/actor"); // Initialize the actor's name and make it an Atlas selectable entity. std::string source(actorName); std::wstring actorNameW = wstring_from_utf8(source); source = "" "" + source + "" // Arbitrary-sized Footprint definition to make actors' selection outlines show up in Atlas. "1.0" "" "" "128x128/ellipse.png128x128/ellipse_mask.png" "" ""; // We'll assume that actorName is valid XML, otherwise this will fail and report the error anyways. CParamNode::LoadXMLString(out, source.c_str(), actorNameW.c_str()); } Index: ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp (revision 26297) +++ ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp (revision 26298) @@ -1,1037 +1,1037 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CCmpRallyPointRenderer.h" #include "ps/Profile.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" std::string CCmpRallyPointRenderer::GetSchema() { return "Displays a rally point marker where created units will gather when spawned" "" "special/rallypoint" "0.75" "round" "square" "" "default" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "0255" "" "" "0255" "" "" "0255" "" "" "" "" "flat" "round" "sharp" "square" "" "" "" "" "flat" "round" "sharp" "square" "" "" "" "" ""; } void CCmpRallyPointRenderer::Init(const CParamNode& paramNode) { m_Displayed = false; m_SmoothPath = true; m_LastOwner = INVALID_PLAYER; m_LastMarkerCount = 0; m_EnableDebugNodeOverlay = false; UpdateLineColor(); // --------------------------------------------------------------------------------------------- // Load some XML configuration data (schema guarantees that all these nodes are valid) m_MarkerTemplate = paramNode.GetChild("MarkerTemplate").ToWString(); const CParamNode& lineDashColor = paramNode.GetChild("LineDashColor"); m_LineDashColor = CColor( lineDashColor.GetChild("@r").ToInt()/255.f, lineDashColor.GetChild("@g").ToInt()/255.f, lineDashColor.GetChild("@b").ToInt()/255.f, 1.f ); m_LineThickness = paramNode.GetChild("LineThickness").ToFixed().ToFloat(); m_LineTexturePath = paramNode.GetChild("LineTexture").ToWString(); m_LineTextureMaskPath = paramNode.GetChild("LineTextureMask").ToWString(); m_LineStartCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineStartCap").ToWString()); m_LineEndCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineEndCap").ToWString()); m_LinePassabilityClass = paramNode.GetChild("LinePassabilityClass").ToString(); // --------------------------------------------------------------------------------------------- // Load some textures if (CRenderer::IsInitialised()) { CTextureProperties texturePropsBase(m_LineTexturePath); texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(4.f); m_Texture = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask(m_LineTextureMaskPath); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(4.f); m_TextureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); } } void CCmpRallyPointRenderer::ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Destroy); componentManager.SubscribeToMessageType(MT_PositionChanged); } void CCmpRallyPointRenderer::Deinit() { } void CCmpRallyPointRenderer::Serialize(ISerializer& UNUSED(serialize)) { // Do NOT serialize anything; this is a rendering-only component, it does not and should not affect simulation state } void CCmpRallyPointRenderer::Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); // The dependent components have not been deserialized, so the color is loaded on first SetDisplayed } void CCmpRallyPointRenderer::HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_PlayerColorChanged: { const CMessagePlayerColorChanged& msgData = static_cast (msg); CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership || msgData.player != cmpOwnership->GetOwner()) break; UpdateLineColor(); ConstructAllOverlayLines(); } break; case MT_RenderSubmit: { PROFILE("RallyPoint::RenderSubmit"); if (m_Displayed && IsSet()) { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); } } break; case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); // Ignore destroyed entities if (msgData.to == INVALID_PLAYER) break; Reset(); // Required for both the initial and capturing players color UpdateLineColor(); // Support capturing, even though RallyPoint is typically deleted then UpdateMarkers(); ConstructAllOverlayLines(); } break; case MT_TurnStart: { UpdateOverlayLines(); // Check for changes to the SoD and update the overlay lines accordingly } break; case MT_Destroy: { Reset(); } break; case MT_PositionChanged: { // Unlikely to happen in-game, but can occur in atlas // Just recompute the path from the entity to the first rally point RecomputeRallyPointPath_wrapper(0); } break; } } void CCmpRallyPointRenderer::UpdateMessageSubscriptions() { GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, m_Displayed && IsSet()); } void CCmpRallyPointRenderer::UpdateMarkers() { player_id_t previousOwner = m_LastOwner; for (size_t i = 0; i < m_RallyPoints.size(); ++i) { if (i >= m_MarkerEntityIds.size()) m_MarkerEntityIds.push_back(INVALID_ENTITY); if (m_MarkerEntityIds[i] == INVALID_ENTITY) { // No marker exists yet, create one first CComponentManager& componentMgr = GetSimContext().GetComponentManager(); // Allocate a new entity for the marker if (!m_MarkerTemplate.empty()) { m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); if (m_MarkerEntityIds[i] != INVALID_ENTITY) m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); } } // The marker entity should be valid at this point, otherwise something went wrong trying to allocate it if (m_MarkerEntityIds[i] == INVALID_ENTITY) LOGERROR("Failed to create rally point marker entity"); CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); if (markerCmpPosition) { if (m_Displayed && IsSet()) { markerCmpPosition->MoveTo(m_RallyPoints[i].X, m_RallyPoints[i].Y); } else { markerCmpPosition->MoveOutOfWorld(); } } // Set rally point flag selection based on player civilization CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership) continue; player_id_t ownerId = cmpOwnership->GetOwner(); if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount >= i)) continue; m_LastOwner = ownerId; CmpPtr cmpPlayerManager(GetSystemEntity()); // cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time // (we can't rely on component initialization order in Init()) if (!cmpPlayerManager) continue; - CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); - if (!cmpPlayer) + CmpPtr cmpIdentity(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); + if (!cmpIdentity) continue; CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); if (cmpVisualActor) - cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); + cmpVisualActor->SetVariant("civ", CStrW(cmpIdentity->GetCiv()).ToUTF8()); } m_LastMarkerCount = m_RallyPoints.size() - 1; } void CCmpRallyPointRenderer::UpdateLineColor() { CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership) return; player_id_t owner = cmpOwnership->GetOwner(); if (owner == INVALID_PLAYER) return; CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(owner)); if (!cmpPlayer) return; m_LineColor = cmpPlayer->GetDisplayedColor(); } void CCmpRallyPointRenderer::RecomputeAllRallyPointPaths() { m_Path.clear(); m_VisibilitySegments.clear(); m_TexturedOverlayLines.clear(); if (m_EnableDebugNodeOverlay) m_DebugNodeOverlays.clear(); // No use computing a path if the rally point isn't set if (!IsSet()) return; CmpPtr cmpPosition(GetEntityHandle()); // No point going on if this entity doesn't have a position or is outside of the world if (!cmpPosition || !cmpPosition->IsInWorld()) return; CmpPtr cmpFootprint(GetEntityHandle()); CmpPtr cmpPathfinder(GetSystemEntity()); for (size_t i = 0; i < m_RallyPoints.size(); ++i) { RecomputeRallyPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); } } void CCmpRallyPointRenderer::RecomputeRallyPointPath_wrapper(size_t index) { // No use computing a path if the rally point isn't set if (!IsSet()) return; // No point going on if this entity doesn't have a position or is outside of the world CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CmpPtr cmpFootprint(GetEntityHandle()); CmpPtr cmpPathfinder(GetSystemEntity()); RecomputeRallyPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); } void CCmpRallyPointRenderer::RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) { while (index >= m_Path.size()) { std::vector tmp; m_Path.push_back(tmp); } m_Path[index].clear(); while (index >= m_VisibilitySegments.size()) { std::vector tmp; m_VisibilitySegments.push_back(tmp); } m_VisibilitySegments[index].clear(); // Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a // list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. WaypointPath path; std::vector& waypoints = path.m_Waypoints; CFixedVector2D start(cmpPosition->GetPosition2D()); PathGoal goal = { PathGoal::POINT, m_RallyPoints[index].X, m_RallyPoints[index].Y }; if (index == 0) GetClosestsEdgePointFrom(start,m_RallyPoints[index], cmpPosition, cmpFootprint); else { start.X = m_RallyPoints[index-1].X; start.Y = m_RallyPoints[index-1].Y; } cmpPathfinder->ComputePathImmediate(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); // Check if we got a path back; if not we probably have two markers less than one tile apart. if (path.m_Waypoints.size() < 2) { m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat()); return; } else if (index == 0) { // Sometimes this ends up not being optimal if you asked for a long path, so improve. CFixedVector2D newend(waypoints[waypoints.size()-2].x,waypoints[waypoints.size()-2].z); GetClosestsEdgePointFrom(newend,newend, cmpPosition, cmpFootprint); waypoints.back().x = newend.X; waypoints.back().z = newend.Y; } else { // Make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. waypoints.back().x = m_RallyPoints[index-1].X; waypoints.back().z = m_RallyPoints[index-1].Y; } // Pathfinder makes us go to the nearest passable cell which isn't always what we want waypoints[0].x = m_RallyPoints[index].X; waypoints[0].z = m_RallyPoints[index].Y; // From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth // between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. // Since we'll only be further using these points for rendering purposes, using floats should be fine. for (Waypoint& waypoint : waypoints) m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); // Post-processing // Linearize the path; // Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path // goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, // loop backwards through the waypoints so that the marker waypoint is maintained. // TODO: see if we can do this at the same time as the waypoint -> coord conversion above for(size_t i = m_Path[index].size() - 2; i > 0; --i) m_Path[index][i] = (m_Path[index][i] + m_Path[index][i-1]) / 2.0f; // Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles // (prevents segments that are too long to properly stick to the terrain) ReduceSegmentsByVisibility(m_Path[index], 6); // Debug overlays if (m_EnableDebugNodeOverlay) { while (index >= m_DebugNodeOverlays.size()) m_DebugNodeOverlays.emplace_back(); m_DebugNodeOverlays[index].clear(); } if (m_EnableDebugNodeOverlay && m_SmoothPath) { // Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the // terrain so we can still see them after the interpolated points are added) for (const CVector2D& point : m_Path[index]) { SOverlayLine overlayLine; overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); overlayLine.m_Thickness = 0.1f; SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); m_DebugNodeOverlays[index].push_back(overlayLine); } } if (m_SmoothPath) // The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together // by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to // generate to be able to deal with local terrain height changes. // no offset, keep line at its exact path SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // Find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn GetVisibilitySegments(m_VisibilitySegments[index], index); // Build overlay lines for the new path ConstructOverlayLines(index); } void CCmpRallyPointRenderer::ConstructAllOverlayLines() { m_TexturedOverlayLines.clear(); for (size_t i = 0; i < m_Path.size(); ++i) ConstructOverlayLines(i); } void CCmpRallyPointRenderer::ConstructOverlayLines(size_t index) { // We need to create a new SOverlayTexturedLine every time we want to change the coordinates after having passed it to the // renderer, because it does some fancy vertex buffering thing and caches them internally instead of recomputing them on every // pass (which is only sensible). while (index >= m_TexturedOverlayLines.size()) { std::vector tmp; m_TexturedOverlayLines.push_back(tmp); } m_TexturedOverlayLines[index].clear(); if (m_Path[index].size() < 2) return; SOverlayTexturedLine::LineCapType dashesLineCapType = SOverlayTexturedLine::LINECAP_ROUND; // line caps to use for the dashed segments (and any other segment's edges that border it) for(const SVisibilitySegment& segment : m_VisibilitySegments[index]) { if (segment.m_Visible) { // Does this segment border on the building or rally point flag on either side? bool bordersBuilding = (segment.m_EndIndex == m_Path[index].size() - 1); bool bordersFlag = (segment.m_StartIndex == 0); // Construct solid textured overlay line along a subset of the full path points from startPointIdx to endPointIdx SOverlayTexturedLine overlayLine; overlayLine.m_Thickness = m_LineThickness; overlayLine.m_SimContext = &GetSimContext(); overlayLine.m_TextureBase = m_Texture; overlayLine.m_TextureMask = m_TextureMask; overlayLine.m_Color = m_LineColor; overlayLine.m_Closed = false; // We should take care to only use m_LineXCap for the actual end points at the building and the rally point; any intermediate // end points (i.e., that border a dashed segment) should have the dashed cap // the path line is actually in reverse order as well, so let's swap out the start and end caps overlayLine.m_StartCapType = (bordersFlag ? m_LineEndCapType : dashesLineCapType); overlayLine.m_EndCapType = (bordersBuilding ? m_LineStartCapType : dashesLineCapType); overlayLine.m_AlwaysVisible = true; // Push overlay line coordinates ENSURE(segment.m_EndIndex > segment.m_StartIndex); // End index is inclusive here for (size_t j = segment.m_StartIndex; j <= segment.m_EndIndex; ++j) overlayLine.m_Coords.push_back(m_Path[index][j]); m_TexturedOverlayLines[index].push_back(overlayLine); } else { // Construct dashed line from startPointIdx to endPointIdx; add textured overlay lines for it to the render list std::vector straightLine; straightLine.push_back(m_Path[index][segment.m_StartIndex]); straightLine.push_back(m_Path[index][segment.m_EndIndex]); // We always want to the dashed line to end at either point with a full dash (i.e. not a cleared space), so that the dashed // area is visually obvious. This requires some calculations to see what size we should make the dashes and clears for them // to fit exactly. float maxDashSize = 3.f; float maxClearSize = 3.f; float dashSize = maxDashSize; float clearSize = maxClearSize; // Ratio of the dash's length to a (dash + clear) pair's length float pairDashRatio = dashSize / (dashSize + clearSize); // Straight-line distance between the points float distance = (m_Path[index][segment.m_StartIndex] - m_Path[index][segment.m_EndIndex]).Length(); // See how many pairs (dash + clear) of unmodified size can fit into the distance. Then check the remaining distance; if it's not exactly // a dash size's worth (which it probably won't be), then adjust the dash/clear sizes slightly so that it is. int numFitUnmodified = floor(distance/(dashSize + clearSize)); float remainderDistance = distance - (numFitUnmodified * (dashSize + clearSize)); // Now we want to make remainderDistance equal exactly one dash size (i.e. maxDashSize) by scaling dashSize and clearSize slightly. // We have (remainderDistance - maxDashSize) of space to distribute over numFitUnmodified instances of (dashSize + clearSize) to make // it fit, so each (dashSize + clearSize) pair needs to adjust its length by (remainderDistance - maxDashSize)/numFitUnmodified // (which will be positive or negative accordingly). This number can then be distributed further proportionally among the dash's // length and the clear's length. // We always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. numFitUnmodified = std::max(1, numFitUnmodified); // Can be either positive or negative float pairwiseLengthDifference = (remainderDistance - maxDashSize)/numFitUnmodified; dashSize += pairDashRatio * pairwiseLengthDifference; clearSize += (1 - pairDashRatio) * pairwiseLengthDifference; // ------------------------------------------------------------------------------------------------ SDashedLine dashedLine; SimRender::ConstructDashedLine(straightLine, dashedLine, dashSize, clearSize); // Build overlay lines for dashes size_t numDashes = dashedLine.m_StartIndices.size(); for (size_t i=0; i < numDashes; i++) { SOverlayTexturedLine dashOverlay; dashOverlay.m_Thickness = m_LineThickness; dashOverlay.m_SimContext = &GetSimContext(); dashOverlay.m_TextureBase = m_Texture; dashOverlay.m_TextureMask = m_TextureMask; dashOverlay.m_Color = m_LineDashColor; dashOverlay.m_Closed = false; dashOverlay.m_StartCapType = dashesLineCapType; dashOverlay.m_EndCapType = dashesLineCapType; dashOverlay.m_AlwaysVisible = true; // TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path size_t dashStartIndex = dashedLine.m_StartIndices[i]; size_t dashEndIndex = dashedLine.GetEndIndex(i); ENSURE(dashEndIndex > dashStartIndex); for (size_t j = dashStartIndex; j < dashEndIndex; ++j) dashOverlay.m_Coords.push_back(dashedLine.m_Points[j]); m_TexturedOverlayLines[index].push_back(dashOverlay); } } } //// ////////////////////////////////////////////// if (m_EnableDebugNodeOverlay) { while (index >= m_DebugNodeOverlays.size()) { std::vector tmp; m_DebugNodeOverlays.push_back(tmp); } for (size_t j = 0; j < m_Path[index].size(); ++j) { SOverlayLine overlayLine; overlayLine.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); overlayLine.m_Thickness = 1; SimRender::ConstructCircleOnGround(GetSimContext(), m_Path[index][j].X, m_Path[index][j].Y, 0.075f, overlayLine, true); m_DebugNodeOverlays[index].push_back(overlayLine); } } //// ////////////////////////////////////////////// } void CCmpRallyPointRenderer::UpdateOverlayLines() { // We should only do this if the rally point is currently being displayed and set inside the world, otherwise it's a massive // waste of time to calculate all this stuff (this method is called every turn) if (!m_Displayed || !IsSet()) return; // See if there have been any changes to the SoD by grabbing the visibility edge points and comparing them to the previous ones std::vector > newVisibilitySegments; for (size_t i = 0; i < m_Path.size(); ++i) { std::vector tmp; newVisibilitySegments.push_back(tmp); GetVisibilitySegments(newVisibilitySegments[i], i); } // Check if the full path changed, then reconstruct all overlay lines, otherwise check if a segment changed and update that. if (m_VisibilitySegments.size() != newVisibilitySegments.size()) { // Save the new visibility segments to compare against next time m_VisibilitySegments = newVisibilitySegments; ConstructAllOverlayLines(); } else { for (size_t i = 0; i < m_VisibilitySegments.size(); ++i) { if (m_VisibilitySegments[i] != newVisibilitySegments[i]) { // The visibility segments have changed, reconstruct the overlay lines to match. NOTE: The path itself doesn't // change, only the overlay lines we construct from it. // Save the new visibility segments to compare against next time m_VisibilitySegments[i] = newVisibilitySegments[i]; ConstructOverlayLines(i); } } } } void CCmpRallyPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const { ENSURE(cmpPosition); ENSURE(cmpFootprint); // Grab the shape and dimensions of the footprint entity_pos_t footprintSize0, footprintSize1, footprintHeight; ICmpFootprint::EShape footprintShape; cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); // Grab the center of the footprint CFixedVector2D center = cmpPosition->GetPosition2D(); switch (footprintShape) { case ICmpFootprint::SQUARE: { // In this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. // The building's footprint could be rotated any which way, so let's get the rotation around the Y axis // and the rotated unit vectors in the X/Z plane of the shape's footprint // (the Footprint itself holds only the outline, the Position holds the orientation) // Sinus and cosinus of the Y axis rotation angle (aka the yaw) fixed s, c; fixed a = cmpPosition->GetRotation().Y; sincos_approx(a, s, c); // Unit vector along the rotated X axis CFixedVector2D u(c, -s); // Unit vector along the rotated Z axis CFixedVector2D v(s, c); CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); result = center + footprintEdgePoint; break; } case ICmpFootprint::CIRCLE: { // In this case, both footprintSize0 and 1 indicate the circle's radius // Transform target to the point nearest on the edge. CFixedVector2D centerVec2D(center.X, center.Y); CFixedVector2D centerToLast(start - centerVec2D); centerToLast.Normalize(); result = centerVec2D + (centerToLast.Multiply(footprintSize0)); break; } } } void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks, bool floating) const { CmpPtr cmpPathFinder(GetSystemEntity()); CmpPtr cmpTerrain(GetSystemEntity()); CmpPtr cmpWaterManager(GetSystemEntity()); ENSURE(cmpPathFinder && cmpTerrain && cmpWaterManager); if (coords.size() < 3) return; // The basic idea is this: starting from a base node, keep checking each individual point along the path to see if there's a visible // line between it and the base point. If so, keep going, otherwise, make the last visible point the new base node and start the same // process from there on until the entire line is checked. The output is the array of base nodes. std::vector newCoords; StationaryOnlyObstructionFilter obstructionFilter; entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness); pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass); // Save the first base node newCoords.push_back(coords[0]); size_t baseNodeIdx = 0; size_t curNodeIdx = 1; float baseNodeY; entity_pos_t baseNodeX; entity_pos_t baseNodeZ; // Set initial base node coords baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); if (floating) baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); while (curNodeIdx < coords.size()) { // This needs to be true at all times, otherwise we're checking visibility between a point and itself. ENSURE(curNodeIdx > baseNodeIdx); entity_pos_t curNodeX = fixed::FromFloat(coords[curNodeIdx].X); entity_pos_t curNodeZ = fixed::FromFloat(coords[curNodeIdx].Y); float curNodeY = cmpTerrain->GetExactGroundLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y); if (floating) curNodeY = std::max(curNodeY, cmpWaterManager->GetExactWaterLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y)); // Find out whether curNode is visible from baseNode (careful; this is in 2D only; terrain height differences are ignored!) bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass); // Since height differences are ignored by CheckMovement, let's call two points visible from one another only if they're at // roughly the same terrain elevation // TODO: this could probably use some tuning curNodeVisible = curNodeVisible && (fabsf(curNodeY - baseNodeY) < 3.f); if (maxSegmentLinks > 0) // Max. amount of node-to-node links to be eliminated (unsigned subtraction is valid because curNodeIdx is always > baseNodeIdx) curNodeVisible = curNodeVisible && ((curNodeIdx - baseNodeIdx) <= maxSegmentLinks); if (!curNodeVisible) { // Current node is not visible from the base node, so the previous one was the last visible point from baseNode and should // hence become the new base node for further iterations. // If curNodeIdx is adjacent to the current baseNode (which is possible due to steep height differences, e.g. hills), then // we should take care not to stay stuck at the current base node if (curNodeIdx > baseNodeIdx + 1) { baseNodeIdx = curNodeIdx - 1; } else { // curNodeIdx == baseNodeIdx + 1 baseNodeIdx = curNodeIdx; // Move the next candidate node one forward so that we don't test a point against itself in the next iteration ++curNodeIdx; } // Add new base node to output list newCoords.push_back(coords[baseNodeIdx]); // Update base node coordinates baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); if (floating) baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); } ++curNodeIdx; } // We always need to add the last point back to the array; if e.g. all the points up to the last one are all visible from the current // base node, then the loop above just ends and no endpoint is ever added to the list. ENSURE(curNodeIdx == coords.size()); newCoords.push_back(coords[coords.size() - 1]); coords.swap(newCoords); } void CCmpRallyPointRenderer::GetVisibilitySegments(std::vector& out, size_t index) const { out.clear(); if (m_Path[index].size() < 2) return; CmpPtr cmpRangeMgr(GetSystemEntity()); player_id_t currentPlayer = static_cast(GetSimContext().GetCurrentDisplayedPlayer()); CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); // Go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start // a new one at the next point. const float cellSize = static_cast(LOS_TILE_SIZE); bool lastVisible = losQuerier.IsExplored( (fixed::FromFloat(m_Path[index][0].X / cellSize)).ToInt_RoundToNearest(), (fixed::FromFloat(m_Path[index][0].Y / cellSize)).ToInt_RoundToNearest() ); // Starting node index of the current segment size_t curSegmentStartIndex = 0; for (size_t k = 1; k < m_Path[index].size(); ++k) { // Grab tile indices for this coord int i = (fixed::FromFloat(m_Path[index][k].X / cellSize)).ToInt_RoundToNearest(); int j = (fixed::FromFloat(m_Path[index][k].Y / cellSize)).ToInt_RoundToNearest(); bool nodeVisible = losQuerier.IsExplored(i, j); if (nodeVisible != lastVisible) { // Visibility changed; write out the segment that was just completed and get ready for the new one out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); curSegmentStartIndex = k - 1; lastVisible = nodeVisible; } } // Terminate the last segment out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); MergeVisibilitySegments(out); } void CCmpRallyPointRenderer::MergeVisibilitySegments(std::vector& segments) { // Scan for single-point segments; if they are inbetween two other segments, delete them and merge the surrounding segments. // If they're at either end of the path, include them in their bordering segment (but only if those bordering segments aren't // themselves single-point segments, because then we would want those to get absorbed by its surrounding ones first). // First scan for absorptions of single-point surrounded segments (i.e. excluding edge segments) size_t numSegments = segments.size(); // WARNING: FOR LOOP TRICKERY AHEAD! for (size_t i = 1; i < numSegments - 1;) { SVisibilitySegment& segment = segments[i]; if (segment.IsSinglePoint()) { // Since the segments' visibility alternates, the surrounding ones should have the same visibility ENSURE(segments[i-1].m_Visible == segments[i+1].m_Visible); // Make previous segment span all the way across to the next segments[i-1].m_EndIndex = segments[i+1].m_EndIndex; // Erase this segment segments.erase(segments.begin() + i); // And the next (we removed [i], so [i+1] is now at position [i]) segments.erase(segments.begin() + i); // We removed 2 segments, so update the loop condition numSegments -= 2; // In the next iteration, i should still point to the segment right after the one that got expanded, which is now // at position i; so don't increment i here } else { ++i; } } ENSURE(numSegments == segments.size()); // Check to see if the first segment needs to be merged with its neighbour if (segments.size() >= 2 && segments[0].IsSinglePoint()) { int firstSegmentStartIndex = segments.front().m_StartIndex; ENSURE(firstSegmentStartIndex == 0); // At this point, the second segment should never be a single-point segment ENSURE(!segments[1].IsSinglePoint()); segments.erase(segments.begin()); segments.front().m_StartIndex = firstSegmentStartIndex; } // check to see if the last segment needs to be merged with its neighbour if (segments.size() >= 2 && segments[segments.size()-1].IsSinglePoint()) { int lastSegmentEndIndex = segments.back().m_EndIndex; // At this point, the second-to-last segment should never be a single-point segment ENSURE(!segments[segments.size()-2].IsSinglePoint()); segments.pop_back(); segments.back().m_EndIndex = lastSegmentEndIndex; } // -------------------------------------------------------------------------------------------------------- // At this point, every segment should have at least 2 points for (size_t i = 0; i < segments.size(); ++i) { ENSURE(!segments[i].IsSinglePoint()); ENSURE(segments[i].m_EndIndex > segments[i].m_StartIndex); } } void CCmpRallyPointRenderer::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { // We only get here if the rally point is set and should be displayed for(std::vector& row : m_TexturedOverlayLines) for (SOverlayTexturedLine& col : row) { if (col.m_Coords.empty()) continue; if (culling && !col.IsVisibleInFrustum(frustum)) continue; collector.Submit(&col); } if (m_EnableDebugNodeOverlay && !m_DebugNodeOverlays.empty()) { for (std::vector& row : m_DebugNodeOverlays) for (SOverlayLine& col : row) if (!col.m_Coords.empty()) collector.Submit(&col); } } void CCmpRallyPointRenderer::AddPosition_wrapper(const CFixedVector2D& pos) { AddPosition(pos, false); } void CCmpRallyPointRenderer::SetPosition(const CFixedVector2D& pos) { if (!(m_RallyPoints.size() == 1 && m_RallyPoints.front() == pos)) { m_RallyPoints.clear(); AddPosition(pos, true); // Don't need to UpdateMessageSubscriptions here since AddPosition already calls it } } void CCmpRallyPointRenderer::UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos) { if (rallyPointId >= m_RallyPoints.size()) return; m_RallyPoints[rallyPointId] = pos; UpdateMarkers(); // Compute a new path for the current, and if existing the next rally point RecomputeRallyPointPath_wrapper(rallyPointId); if (rallyPointId + 1 < m_RallyPoints.size()) RecomputeRallyPointPath_wrapper(rallyPointId + 1); } void CCmpRallyPointRenderer::SetDisplayed(bool displayed) { if (m_Displayed != displayed) { m_Displayed = displayed; // Set color after all dependent components are deserialized if (displayed && m_LineColor.r < 0) { UpdateLineColor(); ConstructAllOverlayLines(); } // Move the markers out of oblivion and back into the real world, or vice-versa UpdateMarkers(); // Check for changes to the SoD and update the overlay lines accordingly. We need to do this here because this method // only takes effect when the display flag is active; we need to pick up changes to the SoD that might have occurred // while this rally point was not being displayed. UpdateOverlayLines(); UpdateMessageSubscriptions(); } } void CCmpRallyPointRenderer::Reset() { for (entity_id_t& componentId : m_MarkerEntityIds) { if (componentId != INVALID_ENTITY) { GetSimContext().GetComponentManager().DestroyComponentsSoon(componentId); componentId = INVALID_ENTITY; } } m_RallyPoints.clear(); m_MarkerEntityIds.clear(); m_LastOwner = INVALID_PLAYER; m_LastMarkerCount = 0; RecomputeAllRallyPointPaths(); UpdateMessageSubscriptions(); } void CCmpRallyPointRenderer::UpdateColor() { UpdateLineColor(); ConstructAllOverlayLines(); } void CCmpRallyPointRenderer::AddPosition(CFixedVector2D pos, bool recompute) { m_RallyPoints.push_back(pos); UpdateMarkers(); if (recompute) RecomputeAllRallyPointPaths(); else RecomputeRallyPointPath_wrapper(m_RallyPoints.size() - 1); UpdateMessageSubscriptions(); } bool CCmpRallyPointRenderer::IsSet() const { return !m_RallyPoints.empty(); } Index: ps/trunk/source/simulation2/components/ICmpIdentity.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpIdentity.h (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpIdentity.h (revision 26298) @@ -1,37 +1,39 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPIDENTITY #define INCLUDED_ICMPIDENTITY #include "simulation2/system/Interface.h" /** * Identity data. * (This interface only includes the functions needed by native code for entity selection) */ class ICmpIdentity : public IComponent { public: virtual std::string GetSelectionGroupName() = 0; virtual std::wstring GetPhenotype() = 0; + virtual std::wstring GetCiv() = 0; + DECLARE_INTERFACE_TYPE(Identity) }; #endif // INCLUDED_ICMPIDENTITY Index: ps/trunk/source/simulation2/components/ICmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpTemplateManager.h (revision 26297) +++ ps/trunk/source/simulation2/components/ICmpTemplateManager.h (revision 26298) @@ -1,130 +1,136 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPTEMPLATEMANAGER #define INCLUDED_ICMPTEMPLATEMANAGER #include "simulation2/system/Interface.h" #include /** * Template manager: Handles the loading of entity template files for the initialisation * and deserialization of entity components. * * Template names are intentionally restricted to ASCII strings for storage/serialization * efficiency (we have a lot of strings so this is significant); * they correspond to filenames so they shouldn't contain non-ASCII anyway. */ class ICmpTemplateManager : public IComponent { public: /** * Loads the template XML file identified by 'templateName' (including inheritance * from parent XML files) for use with a new entity 'ent'. * The returned CParamNode must not be used for any entities other than 'ent'. * * If templateName is of the form "actor|foo" then it will load a default * stationary entity template that uses actor "foo". (This is a convenience to * avoid the need for hundreds of tiny decorative-object entity templates.) * * If templateName is of the form "preview|foo" then it will load a template * based on entity template "foo" with the non-graphical components removed. * (This is for previewing construction/placement of units.) * * If templateName is of the form "corpse|foo" then it will load a template * like "preview|foo" but with corpse-related components included. * * If templateName is of the form "foundation|foo" then it will load a template * based on entity template "foo" with various components removed and a few changed * and added. (This is for constructing foundations of buildings.) * * @return NULL on error */ virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::string& templateName) = 0; /** * Loads the template XML file identified by 'templateName' (including inheritance * from parent XML files). The templateName syntax is the same as LoadTemplate. * * @return NULL on error */ virtual const CParamNode* GetTemplate(const std::string& templateName) = 0; /** * Like GetTemplate, except without doing the XML validation (so it's faster but * may return invalid templates). * * @return NULL on error */ virtual const CParamNode* GetTemplateWithoutValidation(const std::string& templateName) = 0; /** * Check if the template XML file exists, without trying to load it. */ virtual bool TemplateExists(const std::string& templateName) const = 0; /** * Returns the template most recently specified for the entity 'ent'. * Used during deserialization. * * @return NULL on error */ virtual const CParamNode* LoadLatestTemplate(entity_id_t ent) = 0; /** * Returns the name of the template most recently specified for the entity 'ent'. */ virtual std::string GetCurrentTemplateName(entity_id_t ent) const = 0; /** * Returns the list of entities having the specified template. */ virtual std::vector GetEntitiesUsingTemplate(const std::string& templateName) const = 0; /** * Returns a list of strings that could be validly passed as @c templateName to LoadTemplate. * (This includes "actor|foo" etc names). * Intended for use by the map editor. This is likely to be quite slow. */ virtual std::vector FindAllTemplates(bool includeActors) const = 0; /** + * Returns some data of the civs from the templates. + * Intended for use by the map editor. + */ + virtual std::vector> GetCivData() = 0; + + /** * Returns a list of strings that could be validly passed as @c templateName to LoadTemplate. * Intended for use by the AI manager. */ virtual std::vector FindUsedTemplates() const = 0; /** * Permanently disable XML validation (intended solely for test cases). */ virtual void DisableValidation() = 0; /* * TODO: * When an entity changes template (e.g. upgrades) or player ownership, it * should call some Reload(ent, templateName, playerID) function to load its new template. * When a file changes on disk, something should call Reload(templateName). * * Reloading should happen by sending a message to affected components (containing * their new CParamNode), then automatically updating this.template of scripted components. */ DECLARE_INTERFACE_TYPE(TemplateManager) }; #endif // INCLUDED_ICMPTEMPLATEMANAGER Index: ps/trunk/source/tools/entity/checkrefs.pl =================================================================== --- ps/trunk/source/tools/entity/checkrefs.pl (revision 26297) +++ ps/trunk/source/tools/entity/checkrefs.pl (revision 26298) @@ -1,717 +1,715 @@ use strict; use warnings; use Data::Dumper; use File::Find; use XML::Simple; use JSON; use Getopt::Long qw(GetOptions); use lib "."; use Entity; GetOptions ( '--check-unused' => \(my $checkUnused = 0), '--check-map-xml' => \(my $checkMapXml = 0), '--validate-templates' => \(my $validateTemplates = 0), '--mod-to-check=s' => \(my $modToCheck = "public") ); my @files; my @roots; my @deps; # Force and checkMapXml if checkUnused is enabled to avoid false positives. $checkMapXml |= $checkUnused; my $vfsroot = '../../../binaries/data/mods'; my $supportedTextureFormats = 'dds|png'; my $mods = get_mod_dependencies_string($modToCheck); my $mod_list_string = $modToCheck; if ($mods ne "") { $mod_list_string = $mod_list_string."|$mods"; } $mod_list_string = $mod_list_string."|mod"; print("Checking $modToCheck\'s integrity. \n"); print("The following mod(s) will be loaded: $mod_list_string. \n"); my @mods_list = split(/\|/, "$mod_list_string"); sub get_mod_dependencies { my ($mod) = @_; my $modjson = parse_json_file_full_path("$vfsroot/$mod/mod.json"); my $modjsondeps = $modjson->{'dependencies'}; for my $dep (@{$modjsondeps}) { # 0ad's folder isn't named like the mod. if(index($dep, "0ad") != -1) { $dep = "public"; } } return $modjsondeps; } sub get_mod_dependencies_string { my ($mod) = @_; return join( '|',@{get_mod_dependencies($mod)}); } sub vfs_to_physical { my ($vfsPath) = @_; my $fn = vfs_to_relative_to_mods($vfsPath); return "$vfsroot/$fn"; } sub vfs_to_relative_to_mods { my ($vfsPath) = @_; for my $dep (@mods_list) { my $fn = "$dep/$vfsPath"; if (-e "$vfsroot/$fn") { return $fn; } } } sub find_files { my ($vfsPath, $extn) = @_; my @files; my $find_process = sub { return $File::Find::prune = 1 if $_ eq '.svn'; my $n = $File::Find::name; return if /~$/; return unless -f $_; return unless /\.($extn)$/; $n =~ s~\Q$vfsroot\E/($mod_list_string)/~~; push @files, $n; }; for my $dep (@mods_list) { find({ wanted => $find_process },"$vfsroot/$dep/$vfsPath") if -d "$vfsroot/$dep/$vfsPath"; } return @files; } sub parse_json_file_full_path { my ($vfspath) = @_; open my $fh, $vfspath or die "Failed to open '$vfspath': $!"; # decode_json expects a UTF-8 string and doesn't handle BOMs, so we strip those # (see http://trac.wildfiregames.com/ticket/1556) return decode_json(do { local $/; my $file = <$fh>; $file =~ s/^\xEF\xBB\xBF//; $file }); } sub parse_json_file { my ($vfspath) = @_; return parse_json_file_full_path(vfs_to_physical($vfspath)) } sub add_entities { print "Loading entities...\n"; my @entfiles = find_files('simulation/templates', 'xml'); s~^simulation/templates/(.*)\.xml$~$1~ for @entfiles; for my $f (sort @entfiles) { my $path = "simulation/templates/$f.xml"; push @files, $path; my $ent = Entity::load_inherited($f, "$mod_list_string"); if ($ent->{Entity}{'@parent'}) { my @parents = split(/\|/, $ent->{Entity}{'@parent'}{' content'}); for my $parentPath (@parents) { push @deps, [ $path, "simulation/templates/" . $parentPath . ".xml" ]; } } if ($f !~ /^template_/) { push @roots, $path; if ($ent->{Entity}{VisualActor} and $ent->{Entity}{VisualActor}{Actor}) { my $phenotypes = $ent->{Entity}{Identity}{Phenotype}{' content'} || "default"; my @phenotypes = split /\s/,$phenotypes; for my $phenotype (@phenotypes) { # See simulation2/components/CCmpVisualActor.cpp and Identity.js for explanation. my $actorPath = $ent->{Entity}{VisualActor}{Actor}{' content'}; $actorPath =~ s/{phenotype}/$phenotype/g; push @deps, [ $path, "art/actors/" . $actorPath ]; } push @deps, [ $path, "art/actors/" . $ent->{Entity}{VisualActor}{FoundationActor}{' content'} ] if $ent->{Entity}{VisualActor}{FoundationActor}; } if ($ent->{Entity}{Sound}) { my $phenotypes = $ent->{Entity}{Identity}{Phenotype}{' content'} || "default"; my $lang = $ent->{Entity}{Identity}{Lang}{' content'} || "greek"; my @phenotypes = split /\s/,$phenotypes; for my $phenotype (@phenotypes) { for (grep ref($_), values %{$ent->{Entity}{Sound}{SoundGroups}}) { # see simulation/components/Sound.js and Identity.js for explanation my $soundPath = $_->{' content'}; $soundPath =~ s/{phenotype}/$phenotype/g; $soundPath =~ s/{lang}/$lang/g; push @deps, [ $path, "audio/" . $soundPath ]; } } } if ($ent->{Entity}{Identity}) { push @deps, [ $path, "art/textures/ui/session/portraits/" . $ent->{Entity}{Identity}{Icon}{' content'} ] if $ent->{Entity}{Identity}{Icon} and $ent->{Entity}{Identity}{Icon}{' content'} ne ''; } if ($ent->{Entity}{Heal} and $ent->{Entity}{Heal}{RangeOverlay}) { push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Heal}{RangeOverlay}{LineTexture}{' content'} ] if $ent->{Entity}{Heal}{RangeOverlay}{LineTexture} and $ent->{Entity}{Heal}{RangeOverlay}{LineTexture}{' content'} ne ''; push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask}{' content'} ] if $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask} and $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask}{' content'} ne ''; } if ($ent->{Entity}{Selectable} and $ent->{Entity}{Selectable}{Overlay} and $ent->{Entity}{Selectable}{Overlay}{Texture}) { push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture}{' content'} ] if $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture} and $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture}{' content'} ne ''; push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask}{' content'} ] if $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask} and $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask}{' content'} ne ''; } if ($ent->{Entity}{Formation}) { push @deps, [ $path, "art/textures/ui/session/icons/" . $ent->{Entity}{Formation}{Icon}{' content'} ] if $ent->{Entity}{Formation}{Icon} and $ent->{Entity}{Formation}{Icon}{' content'} ne ''; } } } } sub push_variant_dependencies { my ($variant, $f) = @_; push @deps, [ $f, "art/variants/$variant->{file}" ] if $variant->{file}; push @deps, [ $f, "art/meshes/$variant->{mesh}" ] if $variant->{mesh}; push @deps, [ $f, "art/particles/$variant->{particles}{file}" ] if $variant->{particles}{file}; for my $tex (@{$variant->{textures}{texture}}) { push @deps, [ $f, "art/textures/skins/$tex->{file}" ] if $tex->{file}; } for my $prop (@{$variant->{props}{prop}}) { push @deps, [ $f, "art/actors/$prop->{actor}" ] if $prop->{actor}; } for my $anim (@{$variant->{animations}{animation}}) { push @deps, [ $f, "art/animation/$anim->{file}" ] if $anim->{file}; } } sub add_actors { print "Loading actors...\n"; my @actorfiles = find_files('art/actors', 'xml'); for my $f (sort @actorfiles) { push @files, $f; push @roots, $f; my $actor = XMLin(vfs_to_physical($f), ForceArray => [qw(group variant texture prop animation)], KeyAttr => []) or die "Failed to parse '$f': $!"; for my $group (@{$actor->{group}}) { for my $variant (@{$group->{variant}}) { push_variant_dependencies($variant, $f); } } push @deps, [ $f, "art/materials/$actor->{material}" ] if $actor->{material}; } } sub add_variants { print "Loading variants...\n"; my @variantfiles = find_files('art/variants', 'xml'); for my $f (sort @variantfiles) { push @files, $f; push @roots, $f; my $variant = XMLin(vfs_to_physical($f), ForceArray => [qw(texture prop animation)], KeyAttr => []) or die "Failed to parse '$f': $!"; push_variant_dependencies($variant, $f); } } sub add_art { print "Loading art files...\n"; push @files, find_files('art/textures/particles', $supportedTextureFormats); push @files, find_files('art/textures/terrain', $supportedTextureFormats); push @files, find_files('art/textures/skins', $supportedTextureFormats); push @files, find_files('art/meshes', 'pmd|dae'); push @files, find_files('art/animation', 'psa|dae'); } sub add_materials { print "Loading materials...\n"; my @materialfiles = find_files('art/materials', 'xml'); for my $f (sort @materialfiles) { push @files, $f; my $material = XMLin(vfs_to_physical($f), ForceArray => [qw(alternative)], KeyAttr => []); for my $alternative (@{$material->{alternative}}) { push @deps, [ $f, "art/materials/$alternative->{material}" ] if $alternative->{material}; } } } sub add_particles { print "Loading particles...\n"; my @particlefiles = find_files('art/particles', 'xml'); for my $f (sort @particlefiles) { push @files, $f; my $particle = XMLin(vfs_to_physical($f)); push @deps, [ $f, "$particle->{texture}" ] if $particle->{texture}; } } sub add_maps_xml { print "Loading maps XML...\n"; my @mapfiles = find_files('maps/scenarios', 'xml'); push @mapfiles, find_files('maps/skirmishes', 'xml'); push @mapfiles, find_files('maps/tutorials', 'xml'); for my $f (sort @mapfiles) { push @files, $f; push @roots, $f; my $map = XMLin(vfs_to_physical($f), ForceArray => [qw(Entity)], KeyAttr => []) or die "Failed to parse '$f': $!"; my %used; for my $entity (@{$map->{Entities}{Entity}}) { $used{$entity->{Template}} = 1; } for my $template (keys %used) { if ($template =~ /^actor\|(.*)$/) { # Handle special 'actor|' case push @deps, [ $f, "art/actors/$1" ]; } else { if ($template =~ /^resource\|(.*)$/) { # Handle special 'resource|' case $template = $1; } push @deps, [ $f, "simulation/templates/$template.xml" ]; } } # Map previews my $settings = decode_json($map->{ScriptSettings}); push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $settings->{Preview} ] if $settings->{Preview}; } } sub add_maps_pmp { print "Loading maps PMP...\n"; # Need to generate terrain texture filename=>path lookup first my %terrains; for my $f (find_files('art/terrains', 'xml')) { $f =~ /([^\/]+)\.xml/ or die; # ignore terrains.xml if ($f !~ /terrains.xml$/) { warn "Duplicate terrain name '$1' (from '$terrains{$1}' and '$f')\n" if $terrains{$1}; $terrains{$1} = $f; } } my @mapfiles = find_files('maps/scenarios', 'pmp'); push @mapfiles, find_files('maps/skirmishes', 'pmp'); for my $f (sort @mapfiles) { push @files, $f; push @roots, $f; open my $fh, vfs_to_physical($f) or die "Failed to open '$f': $!"; binmode $fh; my $buf; read $fh, $buf, 4; die "Invalid PMP header ($buf) in '$f'" unless $buf eq "PSMP"; read $fh, $buf, 4; my $version = unpack 'V', $buf; die "Invalid PMP version ($version) in '$f'" unless $version == 7; read $fh, $buf, 4; my $datasize = unpack 'V', $buf; read $fh, $buf, 4; my $mapsize = unpack 'V', $buf; seek $fh, 2 * ($mapsize*16+1)*($mapsize*16+1), 1; # heightmap read $fh, $buf, 4; my $numtexs = unpack 'V', $buf; for (0..$numtexs-1) { read $fh, $buf, 4; my $len = unpack 'V', $buf; my $str; read $fh, $str, $len; push @deps, [ $f, $terrains{$str} || "art/terrains/(unknown)/$str" ]; } # ignore patches data } } sub add_soundgroups { print "Loading sound groups...\n"; my @soundfiles = find_files('audio', 'xml'); for my $f (sort @soundfiles) { push @files, $f; push @roots, $f; my $sound = XMLin(vfs_to_physical($f), ForceArray => [qw(Sound)], KeyAttr => []) or die "Failed to parse '$f': $!"; my $path = $sound->{Path}; $path =~ s/\/$//; # strip optional trailing slash for (@{$sound->{Sound}}) { push @deps, [$f, "$path/$_" ]; } } } sub add_audio { print "Loading audio files...\n"; push @files, find_files('audio', 'ogg'); } sub add_gui_xml { print "Loading GUI XML...\n"; my @guifiles = find_files('gui', 'xml'); for my $f (sort @guifiles) { push @files, $f; # GUI page definitions are assumed to be named page[_something].xml and alone in that. if ($f =~ /\/page(_[^.\/]+)?\.xml$/) { push @roots, $f; my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(include)], KeyAttr => []) or die "Failed to parse '$f': $!"; for my $include (@{$xml->{include}}) { # If including an entire directory, find all the *.xml files if ($include =~ /\/$/) { push @deps, [ $f, $_ ] for find_files("gui/$include", 'xml'); } else { push @deps, [ $f, "gui/$include" ]; } } } else { my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(object script action sprite image)], KeyAttr => [], KeepRoot => 1) or die "Failed to parse '$f': $!"; my $name = (keys %$xml)[0]; if ($name eq 'objects' or $name eq 'object') { for (grep ref $_ , @{$xml->{objects}{script}}) { push @deps, [ $f, $_->{file} ] if $_->{file}; if ($_->{directory}) { # If including an entire directory, find all the *.js files push @deps, [ $f, $_ ] for find_files($_->{directory}, 'js') } } my $add_objects; $add_objects = sub { my ($parent) = @_; for my $obj (@{$parent->{object}}) { # TODO: look at sprites, styles, etc $add_objects->($obj); } }; $add_objects->($xml->{objects}); } elsif ($name eq 'setup') { # TODO: look at sprites, styles, etc } elsif ($name eq 'styles') { # TODO: look at sprites, styles, etc } elsif ($name eq 'sprites') { for my $sprite (@{$xml->{sprites}{sprite}}) { for my $image (@{$sprite->{image}}) { push @deps, [ $f, "art/textures/ui/$image->{texture}" ] if $image->{texture}; } } } else { print "Unexpected GUI XML root element '$name':\n" . Dumper $xml; exit; } } } } sub add_gui_data { print "Loading GUI data...\n"; push @files, find_files('gui', 'js'); push @files, find_files('art/textures/ui', $supportedTextureFormats); push @files, find_files('art/textures/selection', $supportedTextureFormats); } sub add_civs { print "Loading civs...\n"; my @civfiles = find_files('simulation/data/civs', 'json'); for my $f (sort @civfiles) { push @files, $f; push @roots, $f; my $civ = parse_json_file($f); - push @deps, [ $f, "art/textures/ui/" . $civ->{Emblem} ] if $civ->{Emblem}; - push @deps, [ $f, "audio/music/" . $_->{File} ] for @{$civ->{Music}}; } } sub add_rms { print "Loading random maps...\n"; push @files, find_files('maps/random', 'js'); my @rmsdefs = find_files('maps/random', 'json'); for my $f (sort @rmsdefs) { next if $f =~ /^maps\/random\/rmbiome/; push @files, $f; push @roots, $f; my $rms = parse_json_file($f); push @deps, [ $f, "maps/random/" . $rms->{settings}{Script} ] if $rms->{settings}{Script}; # Map previews push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $rms->{settings}{Preview} ] if $rms->{settings}{Preview}; } } sub add_techs { print "Loading techs...\n"; my @techfiles = find_files('simulation/data/technologies', 'json'); for my $f (sort @techfiles) { push @files, $f; push @roots, $f; my $tech = parse_json_file($f); push @deps, [ $f, "art/textures/ui/session/portraits/technologies/" . $tech->{icon} ] if $tech->{icon}; push @deps, [ $f, "simulation/data/technologies/" . $tech->{supersedes} . ".json" ] if $tech->{supersedes}; } } sub add_auras { print "Loading auras...\n"; my @aurafiles = find_files('simulation/data/auras', 'json'); for my $f (sort @aurafiles) { push @files, $f; push @roots, $f; my $aura = parse_json_file($f); push @deps, [ $f, $aura->{overlayIcon} ] if $aura->{overlayIcon}; if($aura->{rangeOverlay}) { push @deps, [ $f, "art/textures/selection/" . $aura->{rangeOverlay}{lineTexture} ] if $aura->{rangeOverlay}{lineTexture}; push @deps, [ $f, "art/textures/selection/" . $aura->{rangeOverlay}{lineTextureMask} ] if $aura->{rangeOverlay}{lineTextureMask}; } } } sub add_terrains { print "Loading terrains...\n"; my @terrains = find_files('art/terrains', 'xml'); for my $f (sort @terrains) { # ignore terrains.xml if ($f !~ /terrains.xml$/) { push @files, $f; push @roots, $f; my $terrain = XMLin(vfs_to_physical($f), ForceArray => [qw(texture)], KeyAttr => []) or die "Failed to parse '$f': $!"; for my $texture (@{$terrain->{textures}{texture}}) { push @deps, [ $f, "art/textures/terrain/$texture->{file}" ] if $texture->{file}; } push @deps, [ $f, "art/materials/$terrain->{material}" ] if $terrain->{material}; } } } sub check_deps { my %files; @files{@files} = (); my %lcfiles; @lcfiles{map lc($_), @files} = @files; my %revdeps; for my $d (@deps) { push @{$revdeps{$d->[1]}}, $d->[0]; } for my $f (sort keys %revdeps) { if ($f =~ /simulation\/templates\//) { next if exists $files{$f =~ s/templates\//templates\/special\/filter\//r}; next if exists $files{$f =~ s/templates\//templates\/mixins\//r}; } next if exists $files{$f}; warn "Missing file '$f' referenced by: " . (join ', ', map "'$_'", map vfs_to_relative_to_mods($_), sort @{$revdeps{$f}}) . "\n"; if (exists $lcfiles{lc $f}) { warn "### Case-insensitive match (found '$lcfiles{lc $f}')\n"; } } } sub check_unused { my %reachable; @reachable{@roots} = (); my %deps; for my $d (@deps) { push @{$deps{$d->[0]}}, $d->[1]; } while (1) { my @newreachable; for my $r (keys %reachable) { push @newreachable, grep { not exists $reachable{$_} } @{$deps{$r}}; } last if @newreachable == 0; @reachable{@newreachable} = (); } for my $f (sort @files) { next if exists $reachable{$f} || index($f, "art/terrains/") != -1 || index($f, "maps/random/") != -1 || index($f, "art/materials/") != -1; warn "Unused file '" . vfs_to_relative_to_mods($f) . "'\n"; } } add_maps_xml() if $checkMapXml; add_maps_pmp(); add_entities(); add_actors(); add_variants(); add_art(); add_materials(); add_particles(); add_soundgroups(); add_audio(); add_gui_xml(); add_gui_data(); add_civs(); add_rms(); add_techs(); add_terrains(); add_auras(); check_deps(); check_unused() if $checkUnused; print "\n" if $checkUnused; system("perl ../xmlvalidator/validate.pl") if $validateTemplates;