Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"Assets/Items/Equipment/Maps/Map_Paper_01/Map_Paper_01_folded.xob": "E443663E5241622A",
|
||||
"Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob": "77C9FCF3FB1246E9",
|
||||
"Assets/Items/Equipment/workspaces/paper_map.agr": "83675D0B0AB1B2C3",
|
||||
"Assets/Items/Equipment/workspaces/player_map.asi": "56794534F6B8C3D7",
|
||||
"Assets/Props/Fabric/Flags/Data/Flag_1_2_SovietArmy.emat": "B47B52D9DA68905D",
|
||||
"Assets/Props/Fabric/Flags/Data/Flag_1_2_USA.emat": "610803CC3F3D6649",
|
||||
"Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/Data/EquipmentBoxWooden_Weapon_01_USSR.emat": "7D2E831AFA5CF50D",
|
||||
"Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/EquipmentBoxWooden_Weapon_01.xob": "819ED3266EE5113B",
|
||||
"Assets/Props/Military/Sandbags/Data/Sandbags_01_Burlap_MLOD.emat": "2169E8EB73E1C127",
|
||||
"Assets/Props/Military/Sandbags/Data/Sandbags_01_short_Burlap.emat": "31529E064AA7C917",
|
||||
"Assets/Props/Military/Sandbags/Sandbag_01_short.xob": "268DBDABB3C94AD7",
|
||||
"Assets/Structures/BuildingsParts/Doors/DoorFrame_door_metal_A_RIGHT_EXT_B.xob": "E3BC73BFBC7AA497",
|
||||
"Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob": "0B96D139FEBCA006",
|
||||
"Configs/Callsigns/Callsigns_US.conf": "CB7824D572DD0D5C",
|
||||
"Configs/Callsigns/Callsigns_USSR.conf": "958043CE1648EC43",
|
||||
"Configs/EntityCatalog/US/Characters_EntityCatalog_US.conf": "B56E8B4D4B195559",
|
||||
"Configs/EntityCatalog/US/Groups_EntityCatalog_US.conf": "4E70E7C4C2C17D4A",
|
||||
"Configs/EntityCatalog/US/InventoryItems_EntityCatalog_US.conf": "5F7EC52FC40A03E2",
|
||||
"Configs/EntityCatalog/US/SupplyContainerItems_EntityCatalog_US.conf": "0BF36EFF9FFAF92B",
|
||||
"Configs/EntityCatalog/US/WeaponTripod_EntityCatalog_US.conf": "0E4B62F186F97C43",
|
||||
"Configs/EntityCatalog/USSR/Characters_EntityCatalog_USSR.conf": "3B679F1799D55F6F",
|
||||
"Configs/EntityCatalog/USSR/Groups_EntityCatalog_USSR.conf": "37D5B75AAC3E72EA",
|
||||
"Configs/EntityCatalog/USSR/InventoryItems_EntityCatalog_USSR.conf": "C53421647C3D0D2E",
|
||||
"Configs/EntityCatalog/USSR/SupplyContainerItems_EntityCatalog_USSR.conf": "B4CA6761AC935808",
|
||||
"Configs/EntityCatalog/USSR/WeaponTripod_EntityCatalog_USSR.conf": "DDF17C8C140BD1F2",
|
||||
"Configs/Factions/BLUFOR.conf": "E7B7EA4823DD81F0",
|
||||
"Configs/Factions/FIA.conf": "8053DB656DD18B14",
|
||||
"Configs/Factions/OPFOR.conf": "B122E3B10BB3404B",
|
||||
"Configs/GameOverScreen/BaseGameOverScreensConfig.conf": "651583FFFB7FC765",
|
||||
"Configs/Identities/FactionIdentity_US.conf": "B600946E9ACC17C2",
|
||||
"Configs/Identities/FactionIdentity_USSR.conf": "FBBCD73926989580",
|
||||
"Configs/Identities/Territories/FactionHomeTerritory_US.conf": "B548A5B264939A98",
|
||||
"Configs/Identities/Territories/FactionHomeTerritory_USSR.conf": "31FC78AF00523A99",
|
||||
"Configs/Map/MapPlain.conf": "A786DD4868598F15",
|
||||
"Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf": "8BBC73472092902E",
|
||||
"Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf": "0228A79846FD8A97",
|
||||
"Configs/System/ActionContext/ManualCameraContext.conf": "C538807EEE48BED8",
|
||||
"Configs/System/chimeraInputCommon.conf": "795184CF9AD764DB",
|
||||
"Configs/System/chimeraMenus.conf": "C747AFB6B750CE9A",
|
||||
"Configs/Systems/MissionSystems.conf": "C50579A2EC48E8EF",
|
||||
"Prefabs/AI/Groups/Group_Base.et": "000CD338713F2B5A",
|
||||
"Prefabs/AI/SCR_AIWorld.et": "E0A05C76552E7F58",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_Cycle.et": "35BD6541CBB8AC08",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_Defend.et": "93291E72AC23930F",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_GetIn.et": "712F4795CF8B91C7",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_GetOut.et": "C40316EE26846CAB",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_Move.et": "750A8D1695BD6998",
|
||||
"Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et": "B3E7B8DC2BAB8ACC",
|
||||
"Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et": "86E565387A634103",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Base.et": "836E7E39AAC5888B",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et": "42A502E3BB727CEB",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et": "0F6689B491641155",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et": "2F912ED6E399FF47",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF.et": "36E28C628A2F83CB",
|
||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF_Medic.et": "9A234C5857D92187",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et": "23ADBBC31B6A3DC6",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et": "1C78331E156A3D65",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_HeliPilot.et": "A62FA97C4EC64F14",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et": "BF643BE4ADBDFDD3",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et": "612F43A4D5AE765F",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et": "DCB41B3746FDD1BE",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et": "5436629450D8387A",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et": "333DA6244C7DA34C",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Sharpshooter.et": "976AC400219898FA",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF.et": "2DB452B3EC386B92",
|
||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF_Medic.et": "8CA70597606992EC",
|
||||
"Prefabs/Characters/Footwear/CombatBoots_US_01.et": "DAAFD15478BDE1C3",
|
||||
"Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et": "F545C1BE7C99954A",
|
||||
"Prefabs/Characters/HeadGear/Helmet_PASGT_01/Helmet_PASGT_01_base.et": "8A2B2A544F3ED831",
|
||||
"Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et": "3CCA7A9BB4FD3197",
|
||||
"Prefabs/Characters/Uniforms/Pants_US_BDU.et": "604BB72BE8E023C2",
|
||||
"Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et": "4AD6EB80A8B1FA3A",
|
||||
"Prefabs/Groups/BLUFOR/Group_US_FireTeam.et": "84E5BBAB25EA23E5",
|
||||
"Prefabs/Groups/INDFOR/Group_FIA_FireTeam.et": "5BEA04939D148B1D",
|
||||
"Prefabs/Groups/OPFOR/Group_USSR_Base.et": "8DE0C0830FE0C33D",
|
||||
"Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et": "E552DABF3636C2AD",
|
||||
"Prefabs/Items/Core/Item_Base.et": "C95E11C60810F432",
|
||||
"Prefabs/Items/Equipment/Backpacks/Backpack_Kolobok.et": "41A9C55B61F375F0",
|
||||
"Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et": "243948B23D90BECB",
|
||||
"Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et": "AE578EEA4244D41F",
|
||||
"Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et": "21EF98BFC1EB3793",
|
||||
"Prefabs/Items/Equipment/Kits/RepairKit_01/RepairKit_01_wrench.et": "33B2DFDCD0EBA3DB",
|
||||
"Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et": "13772C903CB5E4F7",
|
||||
"Prefabs/Items/Equipment/Radios/Radio_ANPRC77.et": "9B6B61BB3FE3DFB0",
|
||||
"Prefabs/Items/Equipment/Radios/Radio_R107M.et": "54C68E438DD34265",
|
||||
"Prefabs/Items/Equipment/Radios/Radio_R148.et": "E1A5D4B878AA8980",
|
||||
"Prefabs/Items/Equipment/Watches/Watch_SandY184A.et": "78ED4FEF62BBA728",
|
||||
"Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et": "12D5AD21E383B768",
|
||||
"Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et": "C3F1FA1E2EC2B345",
|
||||
"Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et": "A81F501D3EF6F38E",
|
||||
"Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et": "0D9A5DCF89AE7AA9",
|
||||
"Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et": "527D7C5D2E476BDC",
|
||||
"Prefabs/Items/Medicine/SalineBag_01/SalineBag_US_01.et": "00E36F41CA310E2A",
|
||||
"Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et": "D70216B1B2889129",
|
||||
"Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et": "6D56FED1E55A8F84",
|
||||
"Prefabs/MP/Managers/Factions/FactionManager_Base.et": "A242612865F2A48E",
|
||||
"Prefabs/MP/Managers/Factions/FactionManager_USxUSSR.et": "589540B35EBA8CDC",
|
||||
"Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et": "97EF4C1A41F63DDB",
|
||||
"Prefabs/MP/Modes/GameMode_Base.et": "0F307326459A1395",
|
||||
"Prefabs/MP/ScriptedChatEntity.et": "F69BC912AC8236F9",
|
||||
"Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et": "EBBC859CCD94D96D",
|
||||
"Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE",
|
||||
"Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78",
|
||||
"Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322",
|
||||
"Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993",
|
||||
"Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et": "34B08027DBDE322A",
|
||||
"Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et": "67F17696FF278AE0",
|
||||
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
|
||||
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
|
||||
"Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E",
|
||||
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et": "46904AF5D473FB75",
|
||||
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et": "CAE1588B6265A2AB",
|
||||
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
|
||||
"Prefabs/Systems/Radio/RadioManager.et": "B8E09FAB91C4ECCD",
|
||||
"Prefabs/Tasks/BaseTask.et": "1D0F815858EE24AD",
|
||||
"Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_armed.et": "7BD282AF716ED639",
|
||||
"Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et": "DF5CCB7C0FF049F4",
|
||||
"Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et": "259EE7B78C51B624",
|
||||
"Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et": "0B4DEA8078B78A9B",
|
||||
"Prefabs/Vehicles/Wheeled/UAZ469/VehParts/Seats/UAZ469_seats_rear.et": "780AAF0A51AEE61A",
|
||||
"Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et": "3336BE330C4F355B",
|
||||
"Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et": "16C1F16C9B053801",
|
||||
"Prefabs/Weapons/Ammo/Ammo_Shell_82mm_HE_O832DU.et": "98EC9C526AFBA282",
|
||||
"Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et": "BD496EE1B40DC510",
|
||||
"Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et": "D2018EDB1BBF4C88",
|
||||
"Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et": "C850A33226B8F9C1",
|
||||
"Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et": "7E1DD1CE4D6A23F4",
|
||||
"Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et": "B05A816C0BF50802",
|
||||
"Prefabs/Weapons/Handguns/PM/Handgun_PM.et": "C0F7DD85A86B2900",
|
||||
"Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et": "BBB50A815A2F916B",
|
||||
"Prefabs/Weapons/Magazines/Magazine_762x51_M14_20rnd_SpecialBall.et": "627255315038152A",
|
||||
"Prefabs/Weapons/Magazines/Magazine_762x54_SVD_10rnd_Sniper.et": "9CCB46C6EE632C1A",
|
||||
"Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et": "8B853CDD11BA916E",
|
||||
"Prefabs/Weapons/Mortars/2B14/Mortar_2B14.et": "D1FFE458E8AC4BDB",
|
||||
"Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et": "FA5C25BF66A53DCF",
|
||||
"Prefabs/World/Game/PerceptionManager.et": "028DAEAD63E056BE",
|
||||
"Sounds/Items/_SharedData/PickUp/Items_PickUp_Map.acp": "13F8D0DE67678118",
|
||||
"Sounds/Vehicles/Wheeled/UAZ469/Vehicles_UAZ469_Horn.acp": "7E64D37BE3E1F3FA",
|
||||
"UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt": "3E7733BAC8C831F6",
|
||||
"UI/Textures/DeployMenu/Loadout-BG/Loadout_BG_USSR.imageset": "21533F2089005056",
|
||||
"UI/Textures/DeployMenu/Loadout-BG/Loadout_US_BG.imageset": "F8AC305446D5D480",
|
||||
"UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USA.edds": "EB7F65DBC9392557",
|
||||
"UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USSR.edds": "40B12B0DF911B856",
|
||||
"UI/Textures/Flags/US-flag-base-3it.edds": "46FB2FB2C2125A3C",
|
||||
"UI/Textures/Flags/USSR-flag-base-3it.edds": "50824EB9F2C215AB",
|
||||
"UI/Textures/Icons/icons_mapMarkersUI-glow.imageset": "67B3A6DC2D712B52",
|
||||
"UI/Textures/Icons/icons_mapMarkersUI.imageset": "E23427CAC80DA8B7",
|
||||
"UI/Textures/Icons/icons_wrapperUI-glow.imageset": "00FE3DBDFD15227B",
|
||||
"UI/Textures/Icons/icons_wrapperUI.imageset": "3262679C50EF4F01",
|
||||
"UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout": "C4507D07F3C54831",
|
||||
"UI/layouts/Menus/PauseMenu/PauseMenuButton.layout": "9ECCD201BCF07E95",
|
||||
"UI/layouts/Task/TaskListEntry.layout": "B71975032150BAFB",
|
||||
"UI/layouts/Task/TaskListEntryDescription.layout": "552EB1921B5C548C",
|
||||
"UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout": "75C912A1C89BE6C2",
|
||||
"UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout": "4B5AE6E64037FFB4",
|
||||
"UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout": "0022F0B45ADBC5AC",
|
||||
"system/textures/defWhite.edds": "A64A8C63E0FBAE87",
|
||||
"worlds/Eden/Eden.ent": "853E92315D1D9EFE",
|
||||
"worlds/MP/Navmeshes/LowResEden.nmn": "5E19E60F26327356"
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise exact dependency closure, resource ownership and isolated profile inputs."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import external_addons as ext
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
class ExternalAddonsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
|
||||
self.home=Path(self.temp.name);self.source=self.home/'installed';self.source.mkdir()
|
||||
self.addons=self.home/'profile'/'addons';self.addons.mkdir(parents=True)
|
||||
self.cache=self.home/'profile'/'cache.json';self.value=ext.policy(ROOT)
|
||||
for guid,deps in ext.ALLOWED.items():
|
||||
folder=self.source/guid;folder.mkdir()
|
||||
manifest='GameProject { GUID "'+guid+'" Dependencies { '+' '.join('"'+d+'"' for d in sorted(deps))+' } }\n'
|
||||
(folder/'addon.gproj').write_text(manifest)
|
||||
self.value['addons'][guid]['project_sha256']=hashlib.sha256(manifest.encode()).hexdigest()
|
||||
data=b'RDB synthetic fixture\0'
|
||||
for path,entry in self.value['resources'].items():
|
||||
if entry['addon']==guid:data+=path.encode()+b'\0\x06\0\0\0\0\0'+bytes.fromhex(entry['guid'])[::-1]
|
||||
(folder/'resourceDatabase.rdb').write_bytes(data);(folder/'data.pak').write_bytes(b'fixture '+guid.encode())
|
||||
def test_resource_identity_requires_visible_owner(self):
|
||||
for path,item in self.value['resources'].items():
|
||||
self.assertTrue(ext.resource_allowed(self.value,{item['addon']},item['guid'],path))
|
||||
self.assertFalse(ext.resource_allowed(self.value,{'core'},item['guid'],path))
|
||||
self.assertFalse(ext.resource_allowed(self.value,{item['addon']},'0000000000000000',path))
|
||||
self.assertEqual(ext.direct_for('core'),set(ext.DIRECT));self.assertEqual(ext.direct_for('chat'),set())
|
||||
def test_policy_rejects_original_transitive_dependency(self):
|
||||
root=self.home/'root';(root/'docs').mkdir(parents=True)
|
||||
value=copy.deepcopy(self.value);value['addons'][ext.DIRECT[0]]['dependencies'].append('5F16D7E4A1CBE075')
|
||||
(root/'docs/external-addons.json').write_text(json.dumps(value))
|
||||
with self.assertRaisesRegex(ValueError,'transitive'):ext.policy(root)
|
||||
def test_only_exact_authorized_closure_is_exposed_and_hashed(self):
|
||||
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
|
||||
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
|
||||
self.assertEqual(set(records),set(ext.ALLOWED))
|
||||
self.assertEqual({p.name for p in self.addons.iterdir()},{'ERRI_'+g for g in ext.ALLOWED})
|
||||
self.assertTrue(all(p.is_symlink() for p in self.addons.iterdir()))
|
||||
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
|
||||
self.assertEqual(ext.prepare_profile(self.addons,self.source,self.value,self.cache),records)
|
||||
(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'mutated runtime')
|
||||
self.assertFalse(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
|
||||
def test_missing_and_duplicate_identity_fail(self):
|
||||
guid=ext.DIRECT[0];shutil.copytree(self.source/guid,self.source/'duplicate')
|
||||
with self.assertRaisesRegex(ValueError,'Duplicate'):ext.installed(self.source,self.value)
|
||||
shutil.rmtree(self.source/'duplicate');shutil.rmtree(self.source/guid)
|
||||
with self.assertRaisesRegex(ValueError,'Missing installed'):ext.installed(self.source,self.value)
|
||||
def test_manifest_and_database_mutations_fail(self):
|
||||
guid=ext.DIRECT[0];manifest=self.source/guid/'addon.gproj';old=manifest.read_text();manifest.write_text(old+'// changed')
|
||||
with self.assertRaisesRegex(ValueError,'manifest changed'):ext.installed(self.source,self.value)
|
||||
manifest.write_text(old)
|
||||
resource=next(iter(self.value['resources'].values()));(self.source/resource['addon']/'resourceDatabase.rdb').write_bytes(b'wrong GUID')
|
||||
with self.assertRaisesRegex(ValueError,'resource identity'):ext.installed(self.source,self.value)
|
||||
def test_unrelated_profile_addon_and_wrong_link_fail(self):
|
||||
(self.addons/'original').mkdir()
|
||||
with self.assertRaisesRegex(ValueError,'Unrelated'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
|
||||
(self.addons/'original').rmdir();guid=ext.DIRECT[0]
|
||||
(self.addons/('ERRI_'+guid)).symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError,'Unexpected private'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
|
||||
def test_symlinked_runtime_file_fails(self):
|
||||
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
|
||||
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
+93
File diff suppressed because one or more lines are too long
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preview or write a deterministic DEVELOPMENT SOURCE BUNDLE; never build/publish."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import stat
|
||||
import tempfile
|
||||
import zipfile
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
from source_manifest import addon_manifest
|
||||
from project_schema import parse_project
|
||||
from external_addons import policy as external_policy, direct_for, resource_allowed, ALLOWED
|
||||
from release_check import audit, read_json, evidence_path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BASE = '58D0FB3206B6F859'
|
||||
PROJECTS = {'core': ('ERR_Core', '2528466DE220D91C', ())}
|
||||
|
||||
DIRECTORIES = {'Scripts', 'Prefabs', 'Configs', 'Missions', 'Worlds', 'UI', 'Materials', 'AI'}
|
||||
SUFFIXES = {'.c', '.et', '.conf', '.ent', '.layer', '.layout', '.emat', '.bt', '.meta', '.gproj'}
|
||||
EXCLUDED = {'.git', '.build', '.local', '__pycache__', 'profiles', 'profile', 'cache',
|
||||
'runtime', 'evidence', 'vendor', 'vanilla', 'original', 'node_modules', '.agents', '.codex'}
|
||||
README = '''# DEVELOPMENT SOURCE BUNDLE — Co10 Escape Reintroduced
|
||||
|
||||
Author: Jakub Jim Žáček.
|
||||
|
||||
This ZIP contains the single Co10 Escape Reintroduced addon, with Core, AI,
|
||||
Intel, Map and Chat script folders. It is NOT a native .pak or a published mod.
|
||||
Third-party assets are not redistributed.
|
||||
|
||||
Extract into a NEW development folder and register addons/core/addon.gproj in
|
||||
Workbench. Install and register the RHS/WCS dependencies in EXTERNAL-ADDONS.json.
|
||||
Open addons/core/addon.gproj and use Missions/ERR_Everon.conf.
|
||||
|
||||
Scenario: {C1E20667DCE324ED}Missions/ERR_Everon.conf
|
||||
Use the installed engine's normal project/scenario workflow. This archive has not
|
||||
been compiled by its bundler; no native caches or binaries are included.
|
||||
|
||||
Target: Everon, up to 12 players. The mod requires the listed external addons. This remains development source.
|
||||
Manual prison/scavenging/radio/extraction play, remote joins/reconnects and
|
||||
ownership, twelve connected players, aircraft support, repeated companion cleanup,
|
||||
matched-workload FPS improvement and release qualification require separate
|
||||
source-bound validation. Existing scripted fixtures are not those acceptances.
|
||||
WORKBENCH-only probes are source, not proof of a test run or normal gameplay.
|
||||
|
||||
BUNDLE-MANIFEST.json lists each payload file's SHA-256 and project dependencies.
|
||||
Verify extracted bytes against it before editing. The manifest excludes its own
|
||||
hash to avoid self-reference. These hashes identify bytes, not authenticity,
|
||||
license grants or provenance proof. Source review remains required before sharing.
|
||||
Evidence, acceptance records, runtime profiles and test tools are intentionally
|
||||
absent; consult the maintained repository for current status and retained logs.
|
||||
Even --mode release only adds the existing repository qualification audit before
|
||||
this same development-source handoff; it does not build/publish a game release.
|
||||
'''
|
||||
|
||||
|
||||
def canonical(name):
|
||||
p = PurePosixPath(name)
|
||||
if (not name or p.is_absolute() or p.as_posix() != name or '\\' in name
|
||||
or ':' in name or any(ord(c) < 32 for c in name)
|
||||
or any(part in {'.', '..'} or part.endswith((' ', '.')) for part in p.parts)):
|
||||
raise ValueError(f'unsafe bundle path: {name!r}')
|
||||
return p
|
||||
|
||||
|
||||
def inventory(root):
|
||||
"""Reuse the evidence inventory; impose a narrower reviewed handoff schema."""
|
||||
# Preflight directories without following links, including otherwise excluded
|
||||
# entries themselves. Excluded trees are pruned from this preflight and
|
||||
# excluded from the payload; the shared source manifest may still hash them.
|
||||
addons = root / 'addons'
|
||||
if addons.is_symlink() or not addons.is_dir():
|
||||
raise ValueError('addons must be a real directory')
|
||||
for base, dirs, files in os.walk(addons, followlinks=False):
|
||||
for name in list(dirs) + files:
|
||||
path = Path(base) / name
|
||||
if path.is_symlink():
|
||||
raise ValueError(f'symlink refused: {path.relative_to(root)}')
|
||||
if not path.is_dir() and not stat.S_ISREG(path.lstat().st_mode):
|
||||
raise ValueError(f'nonregular addon entry: {path.relative_to(root)}')
|
||||
dirs[:] = [d for d in dirs if d.lower() not in EXCLUDED]
|
||||
original = addon_manifest(root)
|
||||
result = {}
|
||||
folded = set()
|
||||
for name, digest in original.items():
|
||||
parts = canonical(name).parts
|
||||
if any(part.lower() in EXCLUDED for part in parts):
|
||||
continue
|
||||
if any(part.startswith('.') for part in parts):
|
||||
raise ValueError(f'unreviewed hidden addon content: {name}')
|
||||
if len(parts) < 3 or parts[1] not in PROJECTS:
|
||||
raise ValueError(f'unknown addon project: {name}')
|
||||
path = root / name
|
||||
if not stat.S_ISREG(path.lstat().st_mode):
|
||||
raise ValueError(f'nonregular addon file: {name}')
|
||||
if name.casefold() in folded:
|
||||
raise ValueError(f'case-insensitive path collision: {name}')
|
||||
folded.add(name.casefold())
|
||||
if parts[2] == 'addon.gproj' and len(parts) == 3:
|
||||
pass
|
||||
elif len(parts) < 4 or parts[2] not in DIRECTORIES or path.suffix not in SUFFIXES - {'.gproj'}:
|
||||
raise ValueError(f'unreviewed addon source/resource format: {name}')
|
||||
result[name] = digest
|
||||
return result
|
||||
|
||||
|
||||
def validate(payload, root):
|
||||
projects = {}
|
||||
external = external_policy(root)
|
||||
own_ids = read_json(evidence_path(root, 'docs/addons.json'))
|
||||
if own_ids != {name: item[1] for name, item in PROJECTS.items()}:
|
||||
raise ValueError('successor identity catalog differs from the single owned project')
|
||||
for name, (identity, guid, deps) in PROJECTS.items():
|
||||
text = payload.get(f'addons/{name}/addon.gproj', b'').decode('utf-8')
|
||||
match = parse_project(text, allow_core_menu=name == 'core')
|
||||
expected = {BASE, *(PROJECTS[d][1] for d in deps), *direct_for(name)}
|
||||
found = re.findall(r'"([A-F0-9]{16})"', match[4]) if match else []
|
||||
if not match or match[1] != identity or match[2] != guid or set(found) != expected or len(found) != len(expected):
|
||||
raise ValueError(f'identity/dependency closure mismatch: {name}')
|
||||
projects[name] = {'id': identity, 'guid': guid, 'dependencies': sorted(found)}
|
||||
resources = {}
|
||||
for name, data in payload.items():
|
||||
if not name.endswith('.meta'):
|
||||
continue
|
||||
m = re.search(r'Name "\{([A-F0-9]{16})\}([^"\n]+)"', data.decode('utf-8'))
|
||||
owner = name.split('/')[1]
|
||||
if not m or f'addons/{owner}/{m[2]}.meta' != name or name[:-5] not in payload:
|
||||
raise ValueError(f'mismatched/missing resource metadata: {name}')
|
||||
if m[1] in resources or m[1] in own_ids.values() or m[1] == BASE:
|
||||
raise ValueError(f'duplicate resource identity: {name}')
|
||||
resources[m[1]] = (m[2], owner)
|
||||
vanilla = read_json(evidence_path(root, 'docs/vanilla-resources.json'))
|
||||
if set(resources) & set(vanilla.values()):
|
||||
raise ValueError('successor resource reuses a vanilla identity')
|
||||
for name, data in payload.items():
|
||||
suffix = PurePosixPath(name).suffix
|
||||
if suffix == '.meta':
|
||||
continue
|
||||
text = data.decode('utf-8') # Current reviewed handoff contains text assets only.
|
||||
if re.search(r'\b(?:ESCT\w*|SCRTC_\w+|SCRT_\w+|EAI_\w+)\b', text):
|
||||
raise ValueError(f'original ecosystem dependency: {name}')
|
||||
if suffix not in {'.c', '.layer', '.gproj'} and name + '.meta' not in payload:
|
||||
raise ValueError(f'resource lacks successor metadata: {name}')
|
||||
owner = name.split('/')[1]
|
||||
visible = {owner, *PROJECTS[owner][2]}
|
||||
if 'core' in visible: visible.update(ALLOWED)
|
||||
for guid, resource in re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)', text):
|
||||
if guid in resources:
|
||||
path, target = resources[guid]
|
||||
if path != resource or target not in visible:
|
||||
raise ValueError(f'resource outside project dependency closure: {name}')
|
||||
elif vanilla.get(resource) != guid and not resource_allowed(external,visible,guid,resource):
|
||||
raise ValueError(f'unresolved or nonvanilla resource: {name}')
|
||||
return projects
|
||||
|
||||
|
||||
def build(root=ROOT, mode='development'):
|
||||
root = Path(root).resolve()
|
||||
if mode not in {'development', 'release'}:
|
||||
raise ValueError('unknown bundle mode')
|
||||
if mode == 'release':
|
||||
blockers = audit(root)
|
||||
if blockers:
|
||||
raise ValueError('Not release-qualified:\n - ' + '\n - '.join(blockers))
|
||||
before = inventory(root)
|
||||
payload = {name: (root / name).read_bytes() for name in before}
|
||||
if {name: hashlib.sha256(data).hexdigest() for name, data in payload.items()} != before:
|
||||
raise ValueError('addon bytes changed during collection')
|
||||
projects = validate(payload, root)
|
||||
if mode == 'release':
|
||||
blockers = audit(root)
|
||||
if blockers:
|
||||
raise ValueError('Not release-qualified after collection:\n - ' + '\n - '.join(blockers))
|
||||
if inventory(root) != before:
|
||||
raise ValueError('addon tree changed during collection')
|
||||
payload['BUNDLE-README.md'] = (README + '\nThe combined scenario requires the separately installed authorized addons listed in EXTERNAL-ADDONS.json. Their assets are not bundled.\n').encode('utf-8')
|
||||
payload['EXTERNAL-ADDONS.json'] = json.dumps(external_policy(root),indent=2,sort_keys=True).encode('utf-8')
|
||||
manifest = {'schema_version': 1, 'kind': 'DEVELOPMENT SOURCE BUNDLE',
|
||||
'native_build': False, 'release_package': False, 'projects': projects,
|
||||
'files': {name: hashlib.sha256(data).hexdigest() for name, data in sorted(payload.items())}}
|
||||
payload['BUNDLE-MANIFEST.json'] = (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_STORED) as archive:
|
||||
for name, data in sorted(payload.items()):
|
||||
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
||||
entry.create_system = 3
|
||||
entry.external_attr = (stat.S_IFREG | 0o644) << 16
|
||||
archive.writestr(entry, data)
|
||||
return stream.getvalue(), manifest
|
||||
|
||||
|
||||
def write_new(output, content, root=ROOT):
|
||||
path = Path(output).absolute()
|
||||
canonical(path.as_posix().lstrip('/'))
|
||||
if path.suffix != '.zip' or path.resolve().is_relative_to(Path(root).resolve()):
|
||||
raise ValueError('output must be a new .zip outside the source checkout')
|
||||
for part in (path, *path.parents):
|
||||
if part.is_symlink():
|
||||
raise ValueError('output symlinks are refused')
|
||||
if path.exists() or not path.parent.is_dir():
|
||||
raise ValueError('output exists or parent directory is missing')
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(prefix='.erri-bundle-', dir=path.parent, delete=False) as file:
|
||||
temporary = Path(file.name)
|
||||
file.write(content)
|
||||
file.flush()
|
||||
os.fsync(file.fileno())
|
||||
# Same-filesystem atomic publication; link refuses an existing destination.
|
||||
os.link(temporary, path, follow_symlinks=False)
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--mode', choices=('development', 'release'), default='development')
|
||||
parser.add_argument('--output', type=Path, help='explicit new .zip outside checkout; omitted means read-only preview')
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
content, manifest = build(mode=args.mode)
|
||||
if args.output:
|
||||
write_new(args.output, content)
|
||||
print(json.dumps({'kind': manifest['kind'], 'preview': args.output is None,
|
||||
'payload_files': len(manifest['files']), 'archive_bytes': len(content),
|
||||
'archive_sha256': hashlib.sha256(content).hexdigest(),
|
||||
'projects': manifest['projects']}, indent=2, sort_keys=True))
|
||||
return 0
|
||||
except (OSError, ValueError, UnicodeError) as error:
|
||||
print(f'Bundle refused: {error}')
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user