From 1434f7aa6d8c56e5764392e2c6f2743c9956f605 Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 9 Jun 2025 18:57:53 +0200 Subject: [PATCH] Isometric test --- 3D/Maps/test.map | 25 + ExternalMaterial/Barrel/Barrels.png.import | 13 +- ExternalMaterial/HexGrid/HexGrid.pxc | Bin 27708 -> 33150 bytes ExternalMaterial/HexGrid/HexGrid.pxc1 | Bin 27511 -> 33029 bytes ExternalMaterial/HexGrid/Singles/.gdignore | 0 .../HexGrid/Singles/Floors/Floor0.png | 3 + .../HexGrid/Singles/Floors/Floor0.png.import | 35 + .../HexGrid/Singles/Floors/Floor0.tres | 7 + .../HexGrid/Singles/Floors/Floor1.png | 3 + .../HexGrid/Singles/Floors/Floor1.png.import | 34 + .../HexGrid/Singles/Floors/Floor10.png | 3 + .../HexGrid/Singles/Floors/Floor10.png.import | 34 + .../HexGrid/Singles/Floors/Floor100.png | 3 + .../Singles/Floors/Floor100.png.import | 34 + .../HexGrid/Singles/Floors/Floor101.png | 3 + .../Singles/Floors/Floor101.png.import | 34 + .../HexGrid/Singles/Floors/Floor102.png | 3 + .../Singles/Floors/Floor102.png.import | 34 + .../HexGrid/Singles/Floors/Floor103.png | 3 + .../Singles/Floors/Floor103.png.import | 34 + .../HexGrid/Singles/Floors/Floor104.png | 3 + .../Singles/Floors/Floor104.png.import | 34 + .../HexGrid/Singles/Floors/Floor105.png | 3 + .../Singles/Floors/Floor105.png.import | 34 + .../HexGrid/Singles/Floors/Floor106.png | 3 + .../Singles/Floors/Floor106.png.import | 34 + .../HexGrid/Singles/Floors/Floor107.png | 3 + .../Singles/Floors/Floor107.png.import | 34 + .../HexGrid/Singles/Floors/Floor108.png | 3 + .../Singles/Floors/Floor108.png.import | 34 + .../HexGrid/Singles/Floors/Floor109.png | 3 + .../Singles/Floors/Floor109.png.import | 34 + .../HexGrid/Singles/Floors/Floor11.png | 3 + .../HexGrid/Singles/Floors/Floor11.png.import | 34 + .../HexGrid/Singles/Floors/Floor110.png | 3 + .../Singles/Floors/Floor110.png.import | 34 + .../HexGrid/Singles/Floors/Floor111.png | 3 + .../Singles/Floors/Floor111.png.import | 34 + .../HexGrid/Singles/Floors/Floor112.png | 3 + .../Singles/Floors/Floor112.png.import | 34 + .../HexGrid/Singles/Floors/Floor113.png | 3 + .../Singles/Floors/Floor113.png.import | 34 + .../HexGrid/Singles/Floors/Floor114.png | 3 + .../Singles/Floors/Floor114.png.import | 34 + .../HexGrid/Singles/Floors/Floor115.png | 3 + .../Singles/Floors/Floor115.png.import | 34 + .../HexGrid/Singles/Floors/Floor116.png | 3 + .../Singles/Floors/Floor116.png.import | 34 + .../HexGrid/Singles/Floors/Floor117.png | 3 + .../Singles/Floors/Floor117.png.import | 34 + .../HexGrid/Singles/Floors/Floor118.png | 3 + .../Singles/Floors/Floor118.png.import | 34 + .../HexGrid/Singles/Floors/Floor119.png | 3 + .../Singles/Floors/Floor119.png.import | 34 + .../HexGrid/Singles/Floors/Floor12.png | 3 + .../HexGrid/Singles/Floors/Floor12.png.import | 34 + .../HexGrid/Singles/Floors/Floor120.png | 3 + .../Singles/Floors/Floor120.png.import | 34 + .../HexGrid/Singles/Floors/Floor121.png | 3 + .../Singles/Floors/Floor121.png.import | 34 + .../HexGrid/Singles/Floors/Floor122.png | 3 + .../Singles/Floors/Floor122.png.import | 34 + .../HexGrid/Singles/Floors/Floor123.png | 3 + .../Singles/Floors/Floor123.png.import | 34 + .../HexGrid/Singles/Floors/Floor124.png | 3 + .../Singles/Floors/Floor124.png.import | 34 + .../HexGrid/Singles/Floors/Floor125.png | 3 + .../Singles/Floors/Floor125.png.import | 34 + .../HexGrid/Singles/Floors/Floor126.png | 3 + .../Singles/Floors/Floor126.png.import | 34 + .../HexGrid/Singles/Floors/Floor127.png | 3 + .../Singles/Floors/Floor127.png.import | 34 + .../HexGrid/Singles/Floors/Floor128.png | 3 + .../Singles/Floors/Floor128.png.import | 34 + .../HexGrid/Singles/Floors/Floor129.png | 3 + .../Singles/Floors/Floor129.png.import | 34 + .../HexGrid/Singles/Floors/Floor13.png | 3 + .../HexGrid/Singles/Floors/Floor13.png.import | 34 + .../HexGrid/Singles/Floors/Floor130.png | 3 + .../Singles/Floors/Floor130.png.import | 34 + .../HexGrid/Singles/Floors/Floor131.png | 3 + .../Singles/Floors/Floor131.png.import | 34 + .../HexGrid/Singles/Floors/Floor132.png | 3 + .../Singles/Floors/Floor132.png.import | 34 + .../HexGrid/Singles/Floors/Floor133.png | 3 + .../Singles/Floors/Floor133.png.import | 34 + .../HexGrid/Singles/Floors/Floor134.png | 3 + .../Singles/Floors/Floor134.png.import | 34 + .../HexGrid/Singles/Floors/Floor135.png | 3 + .../Singles/Floors/Floor135.png.import | 34 + .../HexGrid/Singles/Floors/Floor136.png | 3 + .../Singles/Floors/Floor136.png.import | 34 + .../HexGrid/Singles/Floors/Floor137.png | 3 + .../Singles/Floors/Floor137.png.import | 34 + .../HexGrid/Singles/Floors/Floor138.png | 3 + .../Singles/Floors/Floor138.png.import | 34 + .../HexGrid/Singles/Floors/Floor139.png | 3 + .../Singles/Floors/Floor139.png.import | 34 + .../HexGrid/Singles/Floors/Floor14.png | 3 + .../HexGrid/Singles/Floors/Floor14.png.import | 34 + .../HexGrid/Singles/Floors/Floor140.png | 3 + .../Singles/Floors/Floor140.png.import | 34 + .../HexGrid/Singles/Floors/Floor141.png | 3 + .../Singles/Floors/Floor141.png.import | 34 + .../HexGrid/Singles/Floors/Floor142.png | 3 + .../Singles/Floors/Floor142.png.import | 34 + .../HexGrid/Singles/Floors/Floor143.png | 3 + .../Singles/Floors/Floor143.png.import | 34 + .../HexGrid/Singles/Floors/Floor144.png | 3 + .../Singles/Floors/Floor144.png.import | 34 + .../HexGrid/Singles/Floors/Floor145.png | 3 + .../Singles/Floors/Floor145.png.import | 34 + .../HexGrid/Singles/Floors/Floor146.png | 3 + .../Singles/Floors/Floor146.png.import | 34 + .../HexGrid/Singles/Floors/Floor147.png | 3 + .../Singles/Floors/Floor147.png.import | 34 + .../HexGrid/Singles/Floors/Floor148.png | 3 + .../Singles/Floors/Floor148.png.import | 34 + .../HexGrid/Singles/Floors/Floor149.png | 3 + .../Singles/Floors/Floor149.png.import | 34 + .../HexGrid/Singles/Floors/Floor15.png | 3 + .../HexGrid/Singles/Floors/Floor15.png.import | 34 + .../HexGrid/Singles/Floors/Floor150.png | 3 + .../Singles/Floors/Floor150.png.import | 34 + .../HexGrid/Singles/Floors/Floor151.png | 3 + .../Singles/Floors/Floor151.png.import | 34 + .../HexGrid/Singles/Floors/Floor152.png | 3 + .../Singles/Floors/Floor152.png.import | 34 + .../HexGrid/Singles/Floors/Floor153.png | 3 + .../Singles/Floors/Floor153.png.import | 34 + .../HexGrid/Singles/Floors/Floor154.png | 3 + .../Singles/Floors/Floor154.png.import | 34 + .../HexGrid/Singles/Floors/Floor155.png | 3 + .../Singles/Floors/Floor155.png.import | 34 + .../HexGrid/Singles/Floors/Floor156.png | 3 + .../Singles/Floors/Floor156.png.import | 34 + .../HexGrid/Singles/Floors/Floor157.png | 3 + .../Singles/Floors/Floor157.png.import | 34 + .../HexGrid/Singles/Floors/Floor158.png | 3 + .../Singles/Floors/Floor158.png.import | 34 + .../HexGrid/Singles/Floors/Floor159.png | 3 + .../Singles/Floors/Floor159.png.import | 34 + .../HexGrid/Singles/Floors/Floor16.png | 3 + .../HexGrid/Singles/Floors/Floor16.png.import | 34 + .../HexGrid/Singles/Floors/Floor160.png | 3 + .../Singles/Floors/Floor160.png.import | 34 + .../HexGrid/Singles/Floors/Floor161.png | 3 + .../Singles/Floors/Floor161.png.import | 34 + .../HexGrid/Singles/Floors/Floor162.png | 3 + .../Singles/Floors/Floor162.png.import | 34 + .../HexGrid/Singles/Floors/Floor163.png | 3 + .../Singles/Floors/Floor163.png.import | 34 + .../HexGrid/Singles/Floors/Floor164.png | 3 + .../Singles/Floors/Floor164.png.import | 34 + .../HexGrid/Singles/Floors/Floor165.png | 3 + .../Singles/Floors/Floor165.png.import | 34 + .../HexGrid/Singles/Floors/Floor166.png | 3 + .../Singles/Floors/Floor166.png.import | 34 + .../HexGrid/Singles/Floors/Floor167.png | 3 + .../Singles/Floors/Floor167.png.import | 34 + .../HexGrid/Singles/Floors/Floor168.png | 3 + .../Singles/Floors/Floor168.png.import | 34 + .../HexGrid/Singles/Floors/Floor169.png | 3 + .../Singles/Floors/Floor169.png.import | 34 + .../HexGrid/Singles/Floors/Floor17.png | 3 + .../HexGrid/Singles/Floors/Floor17.png.import | 34 + .../HexGrid/Singles/Floors/Floor170.png | 3 + .../Singles/Floors/Floor170.png.import | 34 + .../HexGrid/Singles/Floors/Floor171.png | 3 + .../Singles/Floors/Floor171.png.import | 34 + .../HexGrid/Singles/Floors/Floor172.png | 3 + .../Singles/Floors/Floor172.png.import | 34 + .../HexGrid/Singles/Floors/Floor173.png | 3 + .../Singles/Floors/Floor173.png.import | 34 + .../HexGrid/Singles/Floors/Floor174.png | 3 + .../Singles/Floors/Floor174.png.import | 34 + .../HexGrid/Singles/Floors/Floor175.png | 3 + .../Singles/Floors/Floor175.png.import | 34 + .../HexGrid/Singles/Floors/Floor176.png | 3 + .../Singles/Floors/Floor176.png.import | 34 + .../HexGrid/Singles/Floors/Floor177.png | 3 + .../Singles/Floors/Floor177.png.import | 34 + .../HexGrid/Singles/Floors/Floor178.png | 3 + .../Singles/Floors/Floor178.png.import | 34 + .../HexGrid/Singles/Floors/Floor179.png | 3 + .../Singles/Floors/Floor179.png.import | 34 + .../HexGrid/Singles/Floors/Floor18.png | 3 + .../HexGrid/Singles/Floors/Floor18.png.import | 34 + .../HexGrid/Singles/Floors/Floor180.png | 3 + .../Singles/Floors/Floor180.png.import | 34 + .../HexGrid/Singles/Floors/Floor181.png | 3 + .../Singles/Floors/Floor181.png.import | 34 + .../HexGrid/Singles/Floors/Floor182.png | 3 + .../Singles/Floors/Floor182.png.import | 34 + .../HexGrid/Singles/Floors/Floor183.png | 3 + .../Singles/Floors/Floor183.png.import | 34 + .../HexGrid/Singles/Floors/Floor184.png | 3 + .../Singles/Floors/Floor184.png.import | 34 + .../HexGrid/Singles/Floors/Floor185.png | 3 + .../Singles/Floors/Floor185.png.import | 34 + .../HexGrid/Singles/Floors/Floor186.png | 3 + .../Singles/Floors/Floor186.png.import | 34 + .../HexGrid/Singles/Floors/Floor187.png | 3 + .../Singles/Floors/Floor187.png.import | 34 + .../HexGrid/Singles/Floors/Floor188.png | 3 + .../Singles/Floors/Floor188.png.import | 34 + .../HexGrid/Singles/Floors/Floor189.png | 3 + .../Singles/Floors/Floor189.png.import | 34 + .../HexGrid/Singles/Floors/Floor19.png | 3 + .../HexGrid/Singles/Floors/Floor19.png.import | 34 + .../HexGrid/Singles/Floors/Floor190.png | 3 + .../Singles/Floors/Floor190.png.import | 34 + .../HexGrid/Singles/Floors/Floor191.png | 3 + .../Singles/Floors/Floor191.png.import | 34 + .../HexGrid/Singles/Floors/Floor192.png | 3 + .../Singles/Floors/Floor192.png.import | 34 + .../HexGrid/Singles/Floors/Floor193.png | 3 + .../Singles/Floors/Floor193.png.import | 34 + .../HexGrid/Singles/Floors/Floor194.png | 3 + .../Singles/Floors/Floor194.png.import | 34 + .../HexGrid/Singles/Floors/Floor195.png | 3 + .../Singles/Floors/Floor195.png.import | 34 + .../HexGrid/Singles/Floors/Floor196.png | 3 + .../Singles/Floors/Floor196.png.import | 34 + .../HexGrid/Singles/Floors/Floor197.png | 3 + .../Singles/Floors/Floor197.png.import | 34 + .../HexGrid/Singles/Floors/Floor198.png | 3 + .../Singles/Floors/Floor198.png.import | 34 + .../HexGrid/Singles/Floors/Floor199.png | 3 + .../Singles/Floors/Floor199.png.import | 34 + .../HexGrid/Singles/Floors/Floor2.png | 3 + .../HexGrid/Singles/Floors/Floor2.png.import | 34 + .../HexGrid/Singles/Floors/Floor20.png | 3 + .../HexGrid/Singles/Floors/Floor20.png.import | 34 + .../HexGrid/Singles/Floors/Floor200.png | 3 + .../Singles/Floors/Floor200.png.import | 34 + .../HexGrid/Singles/Floors/Floor201.png | 3 + .../Singles/Floors/Floor201.png.import | 34 + .../HexGrid/Singles/Floors/Floor202.png | 3 + .../Singles/Floors/Floor202.png.import | 34 + .../HexGrid/Singles/Floors/Floor203.png | 3 + .../Singles/Floors/Floor203.png.import | 34 + .../HexGrid/Singles/Floors/Floor204.png | 3 + .../Singles/Floors/Floor204.png.import | 34 + .../HexGrid/Singles/Floors/Floor205.png | 3 + .../Singles/Floors/Floor205.png.import | 34 + .../HexGrid/Singles/Floors/Floor206.png | 3 + .../Singles/Floors/Floor206.png.import | 34 + .../HexGrid/Singles/Floors/Floor207.png | 3 + .../Singles/Floors/Floor207.png.import | 34 + .../HexGrid/Singles/Floors/Floor208.png | 3 + .../Singles/Floors/Floor208.png.import | 34 + .../HexGrid/Singles/Floors/Floor209.png | 3 + .../Singles/Floors/Floor209.png.import | 34 + .../HexGrid/Singles/Floors/Floor21.png | 3 + .../HexGrid/Singles/Floors/Floor21.png.import | 34 + .../HexGrid/Singles/Floors/Floor210.png | 3 + .../Singles/Floors/Floor210.png.import | 34 + .../HexGrid/Singles/Floors/Floor211.png | 3 + .../Singles/Floors/Floor211.png.import | 34 + .../HexGrid/Singles/Floors/Floor212.png | 3 + .../Singles/Floors/Floor212.png.import | 34 + .../HexGrid/Singles/Floors/Floor213.png | 3 + .../Singles/Floors/Floor213.png.import | 34 + .../HexGrid/Singles/Floors/Floor214.png | 3 + .../Singles/Floors/Floor214.png.import | 34 + .../HexGrid/Singles/Floors/Floor215.png | 3 + .../Singles/Floors/Floor215.png.import | 34 + .../HexGrid/Singles/Floors/Floor216.png | 3 + .../Singles/Floors/Floor216.png.import | 34 + .../HexGrid/Singles/Floors/Floor217.png | 3 + .../Singles/Floors/Floor217.png.import | 34 + .../HexGrid/Singles/Floors/Floor218.png | 3 + .../Singles/Floors/Floor218.png.import | 34 + .../HexGrid/Singles/Floors/Floor219.png | 3 + .../Singles/Floors/Floor219.png.import | 34 + .../HexGrid/Singles/Floors/Floor22.png | 3 + .../HexGrid/Singles/Floors/Floor22.png.import | 34 + .../HexGrid/Singles/Floors/Floor220.png | 3 + .../Singles/Floors/Floor220.png.import | 34 + .../HexGrid/Singles/Floors/Floor221.png | 3 + .../Singles/Floors/Floor221.png.import | 34 + .../HexGrid/Singles/Floors/Floor222.png | 3 + .../Singles/Floors/Floor222.png.import | 34 + .../HexGrid/Singles/Floors/Floor223.png | 3 + .../Singles/Floors/Floor223.png.import | 34 + .../HexGrid/Singles/Floors/Floor224.png | 3 + .../Singles/Floors/Floor224.png.import | 34 + .../HexGrid/Singles/Floors/Floor225.png | 3 + .../Singles/Floors/Floor225.png.import | 34 + .../HexGrid/Singles/Floors/Floor226.png | 3 + .../Singles/Floors/Floor226.png.import | 34 + .../HexGrid/Singles/Floors/Floor227.png | 3 + .../Singles/Floors/Floor227.png.import | 34 + .../HexGrid/Singles/Floors/Floor228.png | 3 + .../Singles/Floors/Floor228.png.import | 34 + .../HexGrid/Singles/Floors/Floor229.png | 3 + .../Singles/Floors/Floor229.png.import | 34 + .../HexGrid/Singles/Floors/Floor23.png | 3 + .../HexGrid/Singles/Floors/Floor23.png.import | 34 + .../HexGrid/Singles/Floors/Floor230.png | 3 + .../Singles/Floors/Floor230.png.import | 34 + .../HexGrid/Singles/Floors/Floor231.png | 3 + .../Singles/Floors/Floor231.png.import | 34 + .../HexGrid/Singles/Floors/Floor232.png | 3 + .../Singles/Floors/Floor232.png.import | 34 + .../HexGrid/Singles/Floors/Floor233.png | 3 + .../Singles/Floors/Floor233.png.import | 34 + .../HexGrid/Singles/Floors/Floor234.png | 3 + .../Singles/Floors/Floor234.png.import | 34 + .../HexGrid/Singles/Floors/Floor235.png | 3 + .../Singles/Floors/Floor235.png.import | 34 + .../HexGrid/Singles/Floors/Floor236.png | 3 + .../Singles/Floors/Floor236.png.import | 34 + .../HexGrid/Singles/Floors/Floor237.png | 3 + .../Singles/Floors/Floor237.png.import | 34 + .../HexGrid/Singles/Floors/Floor238.png | 3 + .../Singles/Floors/Floor238.png.import | 34 + .../HexGrid/Singles/Floors/Floor239.png | 3 + .../Singles/Floors/Floor239.png.import | 34 + .../HexGrid/Singles/Floors/Floor24.png | 3 + .../HexGrid/Singles/Floors/Floor24.png.import | 34 + .../HexGrid/Singles/Floors/Floor240.png | 3 + .../Singles/Floors/Floor240.png.import | 34 + .../HexGrid/Singles/Floors/Floor241.png | 3 + .../Singles/Floors/Floor241.png.import | 34 + .../HexGrid/Singles/Floors/Floor242.png | 3 + .../Singles/Floors/Floor242.png.import | 34 + .../HexGrid/Singles/Floors/Floor243.png | 3 + .../Singles/Floors/Floor243.png.import | 34 + .../HexGrid/Singles/Floors/Floor244.png | 3 + .../Singles/Floors/Floor244.png.import | 34 + .../HexGrid/Singles/Floors/Floor245.png | 3 + .../Singles/Floors/Floor245.png.import | 34 + .../HexGrid/Singles/Floors/Floor246.png | 3 + .../Singles/Floors/Floor246.png.import | 34 + .../HexGrid/Singles/Floors/Floor247.png | 3 + .../Singles/Floors/Floor247.png.import | 34 + .../HexGrid/Singles/Floors/Floor248.png | 3 + .../Singles/Floors/Floor248.png.import | 34 + .../HexGrid/Singles/Floors/Floor249.png | 3 + .../Singles/Floors/Floor249.png.import | 34 + .../HexGrid/Singles/Floors/Floor25.png | 3 + .../HexGrid/Singles/Floors/Floor25.png.import | 34 + .../HexGrid/Singles/Floors/Floor250.png | 3 + .../Singles/Floors/Floor250.png.import | 34 + .../HexGrid/Singles/Floors/Floor251.png | 3 + .../Singles/Floors/Floor251.png.import | 34 + .../HexGrid/Singles/Floors/Floor252.png | 3 + .../Singles/Floors/Floor252.png.import | 34 + .../HexGrid/Singles/Floors/Floor253.png | 3 + .../Singles/Floors/Floor253.png.import | 34 + .../HexGrid/Singles/Floors/Floor254.png | 3 + .../Singles/Floors/Floor254.png.import | 34 + .../HexGrid/Singles/Floors/Floor255.png | 3 + .../Singles/Floors/Floor255.png.import | 34 + .../HexGrid/Singles/Floors/Floor256.png | 3 + .../Singles/Floors/Floor256.png.import | 34 + .../HexGrid/Singles/Floors/Floor257.png | 3 + .../Singles/Floors/Floor257.png.import | 34 + .../HexGrid/Singles/Floors/Floor258.png | 3 + .../Singles/Floors/Floor258.png.import | 34 + .../HexGrid/Singles/Floors/Floor259.png | 3 + .../Singles/Floors/Floor259.png.import | 34 + .../HexGrid/Singles/Floors/Floor26.png | 3 + .../HexGrid/Singles/Floors/Floor26.png.import | 34 + .../HexGrid/Singles/Floors/Floor260.png | 3 + .../Singles/Floors/Floor260.png.import | 34 + .../HexGrid/Singles/Floors/Floor261.png | 3 + .../Singles/Floors/Floor261.png.import | 34 + .../HexGrid/Singles/Floors/Floor262.png | 3 + .../Singles/Floors/Floor262.png.import | 34 + .../HexGrid/Singles/Floors/Floor263.png | 3 + .../Singles/Floors/Floor263.png.import | 34 + .../HexGrid/Singles/Floors/Floor264.png | 3 + .../Singles/Floors/Floor264.png.import | 34 + .../HexGrid/Singles/Floors/Floor265.png | 3 + .../Singles/Floors/Floor265.png.import | 34 + .../HexGrid/Singles/Floors/Floor266.png | 3 + .../Singles/Floors/Floor266.png.import | 34 + .../HexGrid/Singles/Floors/Floor267.png | 3 + .../Singles/Floors/Floor267.png.import | 34 + .../HexGrid/Singles/Floors/Floor268.png | 3 + .../Singles/Floors/Floor268.png.import | 34 + .../HexGrid/Singles/Floors/Floor269.png | 3 + .../Singles/Floors/Floor269.png.import | 34 + .../HexGrid/Singles/Floors/Floor27.png | 3 + .../HexGrid/Singles/Floors/Floor27.png.import | 34 + .../HexGrid/Singles/Floors/Floor270.png | 3 + .../Singles/Floors/Floor270.png.import | 34 + .../HexGrid/Singles/Floors/Floor271.png | 3 + .../Singles/Floors/Floor271.png.import | 34 + .../HexGrid/Singles/Floors/Floor272.png | 3 + .../Singles/Floors/Floor272.png.import | 34 + .../HexGrid/Singles/Floors/Floor273.png | 3 + .../Singles/Floors/Floor273.png.import | 34 + .../HexGrid/Singles/Floors/Floor274.png | 3 + .../Singles/Floors/Floor274.png.import | 34 + .../HexGrid/Singles/Floors/Floor275.png | 3 + .../Singles/Floors/Floor275.png.import | 34 + .../HexGrid/Singles/Floors/Floor276.png | 3 + .../Singles/Floors/Floor276.png.import | 34 + .../HexGrid/Singles/Floors/Floor277.png | 3 + .../Singles/Floors/Floor277.png.import | 34 + .../HexGrid/Singles/Floors/Floor278.png | 3 + .../Singles/Floors/Floor278.png.import | 34 + .../HexGrid/Singles/Floors/Floor279.png | 3 + .../Singles/Floors/Floor279.png.import | 34 + .../HexGrid/Singles/Floors/Floor28.png | 3 + .../HexGrid/Singles/Floors/Floor28.png.import | 34 + .../HexGrid/Singles/Floors/Floor280.png | 3 + .../Singles/Floors/Floor280.png.import | 34 + .../HexGrid/Singles/Floors/Floor281.png | 3 + .../Singles/Floors/Floor281.png.import | 34 + .../HexGrid/Singles/Floors/Floor282.png | 3 + .../Singles/Floors/Floor282.png.import | 34 + .../HexGrid/Singles/Floors/Floor283.png | 3 + .../Singles/Floors/Floor283.png.import | 34 + .../HexGrid/Singles/Floors/Floor284.png | 3 + .../Singles/Floors/Floor284.png.import | 34 + .../HexGrid/Singles/Floors/Floor285.png | 3 + .../Singles/Floors/Floor285.png.import | 34 + .../HexGrid/Singles/Floors/Floor286.png | 3 + .../Singles/Floors/Floor286.png.import | 34 + .../HexGrid/Singles/Floors/Floor287.png | 3 + .../Singles/Floors/Floor287.png.import | 34 + .../HexGrid/Singles/Floors/Floor288.png | 3 + .../Singles/Floors/Floor288.png.import | 34 + .../HexGrid/Singles/Floors/Floor289.png | 3 + .../Singles/Floors/Floor289.png.import | 34 + .../HexGrid/Singles/Floors/Floor29.png | 3 + .../HexGrid/Singles/Floors/Floor29.png.import | 34 + .../HexGrid/Singles/Floors/Floor290.png | 3 + .../Singles/Floors/Floor290.png.import | 34 + .../HexGrid/Singles/Floors/Floor291.png | 3 + .../Singles/Floors/Floor291.png.import | 34 + .../HexGrid/Singles/Floors/Floor292.png | 3 + .../Singles/Floors/Floor292.png.import | 34 + .../HexGrid/Singles/Floors/Floor293.png | 3 + .../Singles/Floors/Floor293.png.import | 34 + .../HexGrid/Singles/Floors/Floor294.png | 3 + .../Singles/Floors/Floor294.png.import | 34 + .../HexGrid/Singles/Floors/Floor295.png | 3 + .../Singles/Floors/Floor295.png.import | 34 + .../HexGrid/Singles/Floors/Floor296.png | 3 + .../Singles/Floors/Floor296.png.import | 34 + .../HexGrid/Singles/Floors/Floor297.png | 3 + .../Singles/Floors/Floor297.png.import | 34 + .../HexGrid/Singles/Floors/Floor298.png | 3 + .../Singles/Floors/Floor298.png.import | 34 + .../HexGrid/Singles/Floors/Floor299.png | 3 + .../Singles/Floors/Floor299.png.import | 34 + .../HexGrid/Singles/Floors/Floor3.png | 3 + .../HexGrid/Singles/Floors/Floor3.png.import | 34 + .../HexGrid/Singles/Floors/Floor30.png | 3 + .../HexGrid/Singles/Floors/Floor30.png.import | 34 + .../HexGrid/Singles/Floors/Floor300.png | 3 + .../Singles/Floors/Floor300.png.import | 34 + .../HexGrid/Singles/Floors/Floor301.png | 3 + .../Singles/Floors/Floor301.png.import | 34 + .../HexGrid/Singles/Floors/Floor302.png | 3 + .../Singles/Floors/Floor302.png.import | 34 + .../HexGrid/Singles/Floors/Floor303.png | 3 + .../Singles/Floors/Floor303.png.import | 34 + .../HexGrid/Singles/Floors/Floor304.png | 3 + .../Singles/Floors/Floor304.png.import | 34 + .../HexGrid/Singles/Floors/Floor305.png | 3 + .../Singles/Floors/Floor305.png.import | 34 + .../HexGrid/Singles/Floors/Floor306.png | 3 + .../Singles/Floors/Floor306.png.import | 34 + .../HexGrid/Singles/Floors/Floor307.png | 3 + .../Singles/Floors/Floor307.png.import | 34 + .../HexGrid/Singles/Floors/Floor308.png | 3 + .../Singles/Floors/Floor308.png.import | 34 + .../HexGrid/Singles/Floors/Floor309.png | 3 + .../Singles/Floors/Floor309.png.import | 34 + .../HexGrid/Singles/Floors/Floor31.png | 3 + .../HexGrid/Singles/Floors/Floor31.png.import | 34 + .../HexGrid/Singles/Floors/Floor310.png | 3 + .../Singles/Floors/Floor310.png.import | 34 + .../HexGrid/Singles/Floors/Floor311.png | 3 + .../Singles/Floors/Floor311.png.import | 34 + .../HexGrid/Singles/Floors/Floor312.png | 3 + .../Singles/Floors/Floor312.png.import | 34 + .../HexGrid/Singles/Floors/Floor313.png | 3 + .../Singles/Floors/Floor313.png.import | 34 + .../HexGrid/Singles/Floors/Floor314.png | 3 + .../Singles/Floors/Floor314.png.import | 34 + .../HexGrid/Singles/Floors/Floor315.png | 3 + .../Singles/Floors/Floor315.png.import | 34 + .../HexGrid/Singles/Floors/Floor316.png | 3 + .../Singles/Floors/Floor316.png.import | 34 + .../HexGrid/Singles/Floors/Floor317.png | 3 + .../Singles/Floors/Floor317.png.import | 34 + .../HexGrid/Singles/Floors/Floor318.png | 3 + .../Singles/Floors/Floor318.png.import | 34 + .../HexGrid/Singles/Floors/Floor319.png | 3 + .../Singles/Floors/Floor319.png.import | 34 + .../HexGrid/Singles/Floors/Floor32.png | 3 + .../HexGrid/Singles/Floors/Floor32.png.import | 34 + .../HexGrid/Singles/Floors/Floor33.png | 3 + .../HexGrid/Singles/Floors/Floor33.png.import | 34 + .../HexGrid/Singles/Floors/Floor34.png | 3 + .../HexGrid/Singles/Floors/Floor34.png.import | 34 + .../HexGrid/Singles/Floors/Floor35.png | 3 + .../HexGrid/Singles/Floors/Floor35.png.import | 34 + .../HexGrid/Singles/Floors/Floor36.png | 3 + .../HexGrid/Singles/Floors/Floor36.png.import | 34 + .../HexGrid/Singles/Floors/Floor37.png | 3 + .../HexGrid/Singles/Floors/Floor37.png.import | 34 + .../HexGrid/Singles/Floors/Floor38.png | 3 + .../HexGrid/Singles/Floors/Floor38.png.import | 34 + .../HexGrid/Singles/Floors/Floor39.png | 3 + .../HexGrid/Singles/Floors/Floor39.png.import | 34 + .../HexGrid/Singles/Floors/Floor4.png | 3 + .../HexGrid/Singles/Floors/Floor4.png.import | 34 + .../HexGrid/Singles/Floors/Floor40.png | 3 + .../HexGrid/Singles/Floors/Floor40.png.import | 34 + .../HexGrid/Singles/Floors/Floor41.png | 3 + .../HexGrid/Singles/Floors/Floor41.png.import | 34 + .../HexGrid/Singles/Floors/Floor42.png | 3 + .../HexGrid/Singles/Floors/Floor42.png.import | 34 + .../HexGrid/Singles/Floors/Floor43.png | 3 + .../HexGrid/Singles/Floors/Floor43.png.import | 34 + .../HexGrid/Singles/Floors/Floor44.png | 3 + .../HexGrid/Singles/Floors/Floor44.png.import | 34 + .../HexGrid/Singles/Floors/Floor45.png | 3 + .../HexGrid/Singles/Floors/Floor45.png.import | 34 + .../HexGrid/Singles/Floors/Floor46.png | 3 + .../HexGrid/Singles/Floors/Floor46.png.import | 34 + .../HexGrid/Singles/Floors/Floor47.png | 3 + .../HexGrid/Singles/Floors/Floor47.png.import | 34 + .../HexGrid/Singles/Floors/Floor48.png | 3 + .../HexGrid/Singles/Floors/Floor48.png.import | 34 + .../HexGrid/Singles/Floors/Floor49.png | 3 + .../HexGrid/Singles/Floors/Floor49.png.import | 34 + .../HexGrid/Singles/Floors/Floor5.png | 3 + .../HexGrid/Singles/Floors/Floor5.png.import | 34 + .../HexGrid/Singles/Floors/Floor50.png | 3 + .../HexGrid/Singles/Floors/Floor50.png.import | 34 + .../HexGrid/Singles/Floors/Floor51.png | 3 + .../HexGrid/Singles/Floors/Floor51.png.import | 34 + .../HexGrid/Singles/Floors/Floor52.png | 3 + .../HexGrid/Singles/Floors/Floor52.png.import | 34 + .../HexGrid/Singles/Floors/Floor53.png | 3 + .../HexGrid/Singles/Floors/Floor53.png.import | 34 + .../HexGrid/Singles/Floors/Floor54.png | 3 + .../HexGrid/Singles/Floors/Floor54.png.import | 34 + .../HexGrid/Singles/Floors/Floor55.png | 3 + .../HexGrid/Singles/Floors/Floor55.png.import | 34 + .../HexGrid/Singles/Floors/Floor56.png | 3 + .../HexGrid/Singles/Floors/Floor56.png.import | 34 + .../HexGrid/Singles/Floors/Floor57.png | 3 + .../HexGrid/Singles/Floors/Floor57.png.import | 34 + .../HexGrid/Singles/Floors/Floor58.png | 3 + .../HexGrid/Singles/Floors/Floor58.png.import | 34 + .../HexGrid/Singles/Floors/Floor59.png | 3 + .../HexGrid/Singles/Floors/Floor59.png.import | 34 + .../HexGrid/Singles/Floors/Floor6.png | 3 + .../HexGrid/Singles/Floors/Floor6.png.import | 34 + .../HexGrid/Singles/Floors/Floor60.png | 3 + .../HexGrid/Singles/Floors/Floor60.png.import | 34 + .../HexGrid/Singles/Floors/Floor61.png | 3 + .../HexGrid/Singles/Floors/Floor61.png.import | 34 + .../HexGrid/Singles/Floors/Floor62.png | 3 + .../HexGrid/Singles/Floors/Floor62.png.import | 34 + .../HexGrid/Singles/Floors/Floor63.png | 3 + .../HexGrid/Singles/Floors/Floor63.png.import | 34 + .../HexGrid/Singles/Floors/Floor64.png | 3 + .../HexGrid/Singles/Floors/Floor64.png.import | 34 + .../HexGrid/Singles/Floors/Floor65.png | 3 + .../HexGrid/Singles/Floors/Floor65.png.import | 34 + .../HexGrid/Singles/Floors/Floor66.png | 3 + .../HexGrid/Singles/Floors/Floor66.png.import | 34 + .../HexGrid/Singles/Floors/Floor67.png | 3 + .../HexGrid/Singles/Floors/Floor67.png.import | 34 + .../HexGrid/Singles/Floors/Floor68.png | 3 + .../HexGrid/Singles/Floors/Floor68.png.import | 34 + .../HexGrid/Singles/Floors/Floor69.png | 3 + .../HexGrid/Singles/Floors/Floor69.png.import | 34 + .../HexGrid/Singles/Floors/Floor7.png | 3 + .../HexGrid/Singles/Floors/Floor7.png.import | 34 + .../HexGrid/Singles/Floors/Floor70.png | 3 + .../HexGrid/Singles/Floors/Floor70.png.import | 34 + .../HexGrid/Singles/Floors/Floor71.png | 3 + .../HexGrid/Singles/Floors/Floor71.png.import | 34 + .../HexGrid/Singles/Floors/Floor72.png | 3 + .../HexGrid/Singles/Floors/Floor72.png.import | 34 + .../HexGrid/Singles/Floors/Floor73.png | 3 + .../HexGrid/Singles/Floors/Floor73.png.import | 34 + .../HexGrid/Singles/Floors/Floor74.png | 3 + .../HexGrid/Singles/Floors/Floor74.png.import | 34 + .../HexGrid/Singles/Floors/Floor75.png | 3 + .../HexGrid/Singles/Floors/Floor75.png.import | 34 + .../HexGrid/Singles/Floors/Floor76.png | 3 + .../HexGrid/Singles/Floors/Floor76.png.import | 34 + .../HexGrid/Singles/Floors/Floor77.png | 3 + .../HexGrid/Singles/Floors/Floor77.png.import | 34 + .../HexGrid/Singles/Floors/Floor78.png | 3 + .../HexGrid/Singles/Floors/Floor78.png.import | 34 + .../HexGrid/Singles/Floors/Floor79.png | 3 + .../HexGrid/Singles/Floors/Floor79.png.import | 34 + .../HexGrid/Singles/Floors/Floor8.png | 3 + .../HexGrid/Singles/Floors/Floor8.png.import | 34 + .../HexGrid/Singles/Floors/Floor80.png | 3 + .../HexGrid/Singles/Floors/Floor80.png.import | 34 + .../HexGrid/Singles/Floors/Floor81.png | 3 + .../HexGrid/Singles/Floors/Floor81.png.import | 34 + .../HexGrid/Singles/Floors/Floor82.png | 3 + .../HexGrid/Singles/Floors/Floor82.png.import | 34 + .../HexGrid/Singles/Floors/Floor83.png | 3 + .../HexGrid/Singles/Floors/Floor83.png.import | 34 + .../HexGrid/Singles/Floors/Floor84.png | 3 + .../HexGrid/Singles/Floors/Floor84.png.import | 34 + .../HexGrid/Singles/Floors/Floor85.png | 3 + .../HexGrid/Singles/Floors/Floor85.png.import | 34 + .../HexGrid/Singles/Floors/Floor86.png | 3 + .../HexGrid/Singles/Floors/Floor86.png.import | 34 + .../HexGrid/Singles/Floors/Floor87.png | 3 + .../HexGrid/Singles/Floors/Floor87.png.import | 34 + .../HexGrid/Singles/Floors/Floor88.png | 3 + .../HexGrid/Singles/Floors/Floor88.png.import | 34 + .../HexGrid/Singles/Floors/Floor89.png | 3 + .../HexGrid/Singles/Floors/Floor89.png.import | 34 + .../HexGrid/Singles/Floors/Floor9.png | 3 + .../HexGrid/Singles/Floors/Floor9.png.import | 34 + .../HexGrid/Singles/Floors/Floor90.png | 3 + .../HexGrid/Singles/Floors/Floor90.png.import | 34 + .../HexGrid/Singles/Floors/Floor91.png | 3 + .../HexGrid/Singles/Floors/Floor91.png.import | 34 + .../HexGrid/Singles/Floors/Floor92.png | 3 + .../HexGrid/Singles/Floors/Floor92.png.import | 34 + .../HexGrid/Singles/Floors/Floor93.png | 3 + .../HexGrid/Singles/Floors/Floor93.png.import | 34 + .../HexGrid/Singles/Floors/Floor94.png | 3 + .../HexGrid/Singles/Floors/Floor94.png.import | 34 + .../HexGrid/Singles/Floors/Floor95.png | 3 + .../HexGrid/Singles/Floors/Floor95.png.import | 34 + .../HexGrid/Singles/Floors/Floor96.png | 3 + .../HexGrid/Singles/Floors/Floor96.png.import | 34 + .../HexGrid/Singles/Floors/Floor97.png | 3 + .../HexGrid/Singles/Floors/Floor97.png.import | 34 + .../HexGrid/Singles/Floors/Floor98.png | 3 + .../HexGrid/Singles/Floors/Floor98.png.import | 34 + .../HexGrid/Singles/Floors/Floor99.png | 3 + .../HexGrid/Singles/Floors/Floor99.png.import | 34 + .../HexGrid/Singles/Various/Various0.png | 3 + .../Singles/Various/Various0.png.import | 34 + .../HexGrid/Singles/Various/Various1.png | 3 + .../Singles/Various/Various1.png.import | 34 + .../HexGrid/Singles/Various/Various10.png | 3 + .../Singles/Various/Various10.png.import | 34 + .../HexGrid/Singles/Various/Various11.png | 3 + .../Singles/Various/Various11.png.import | 34 + .../HexGrid/Singles/Various/Various12.png | 3 + .../Singles/Various/Various12.png.import | 34 + .../HexGrid/Singles/Various/Various13.png | 3 + .../Singles/Various/Various13.png.import | 34 + .../HexGrid/Singles/Various/Various14.png | 3 + .../Singles/Various/Various14.png.import | 34 + .../HexGrid/Singles/Various/Various15.png | 3 + .../Singles/Various/Various15.png.import | 34 + .../HexGrid/Singles/Various/Various16.png | 3 + .../Singles/Various/Various16.png.import | 34 + .../HexGrid/Singles/Various/Various17.png | 3 + .../Singles/Various/Various17.png.import | 34 + .../HexGrid/Singles/Various/Various2.png | 3 + .../Singles/Various/Various2.png.import | 34 + .../HexGrid/Singles/Various/Various3.png | 3 + .../Singles/Various/Various3.png.import | 34 + .../HexGrid/Singles/Various/Various4.png | 3 + .../Singles/Various/Various4.png.import | 34 + .../HexGrid/Singles/Various/Various5.png | 3 + .../Singles/Various/Various5.png.import | 34 + .../HexGrid/Singles/Various/Various6.png | 3 + .../Singles/Various/Various6.png.import | 34 + .../HexGrid/Singles/Various/Various7.png | 3 + .../Singles/Various/Various7.png.import | 34 + .../HexGrid/Singles/Various/Various8.png | 3 + .../Singles/Various/Various8.png.import | 34 + .../HexGrid/Singles/Various/Various9.png | 3 + .../Singles/Various/Various9.png.import | 34 + .../HexGrid/Singles/Walls/Wall0.png | 3 + .../HexGrid/Singles/Walls/Wall0.png.import | 35 + .../HexGrid/Singles/Walls/Wall0.tres | 7 + .../HexGrid/Singles/Walls/Wall1.png | 3 + .../HexGrid/Singles/Walls/Wall1.png.import | 34 + .../HexGrid/Singles/Walls/Wall2.png | 3 + .../HexGrid/Singles/Walls/Wall2.png.import | 34 + .../HexGrid/Singles/Walls/Wall3.png | 3 + .../HexGrid/Singles/Walls/Wall3.png.import | 34 + IsoTest/IsoGrid.cs | 16 + IsoTest/IsoGrid.cs.uid | 1 + IsoTest/IsoMapTest1.tscn | 3 + IsoTest/IsoMapTest2.tscn | 461 +++++++ IsoTest/movable.png | 3 + IsoTest/movable.png.import | 34 + Scenes/Maps/PlayerFSMTest.tscn | 2 +- Sprites/Actors/Cirno.png.import | 13 +- .../material_viewer_state_res.tres | 4 +- addons/func_godot/fgd/func_detail.tres | 30 + .../fgd/func_detail_illusionary.tres | 30 + addons/func_godot/fgd/func_geo.tres | 30 + addons/func_godot/fgd/func_godot_fgd.tres | 17 + addons/func_godot/fgd/func_illusionary.tres | 30 + addons/func_godot/fgd/phong_base.tres | 27 + addons/func_godot/fgd/worldspawn.tres | 29 + .../func_godot_default_map_settings.tres | 31 + .../func_godot/func_godot_local_config.tres | 7 + ...dot_netradiant_custom_gamepack_config.tres | 24 + .../netradiant_custom_shader_clip.tres | 8 + .../netradiant_custom_shader_origin.tres | 8 + .../netradiant_custom_shader_skip.tres | 8 + .../func_godot_tb_game_config.tres | 35 + .../trenchbroom/tb_brush_tag_func.tres | 11 + .../trenchbroom/tb_brush_tag_trigger.tres | 11 + .../trenchbroom/tb_face_tag_clip.tres | 11 + .../trenchbroom/tb_face_tag_origin.tres | 11 + .../trenchbroom/tb_face_tag_skip.tres | 11 + addons/func_godot/icon.png | 3 + addons/func_godot/icon.png.import | 34 + addons/func_godot/icon.svg | 13 + addons/func_godot/icon.svg.import | 37 + addons/func_godot/icon32.png | 3 + addons/func_godot/icon32.png.import | 34 + addons/func_godot/icons/icon_godambler.svg | 13 + .../icons/icon_godambler.svg.import | 37 + addons/func_godot/icons/icon_godambler3d.svg | 34 + .../icons/icon_godambler3d.svg.import | 37 + addons/func_godot/icons/icon_godot_ranger.svg | 13 + .../icons/icon_godot_ranger.svg.import | 37 + .../func_godot/icons/icon_godot_ranger3d.svg | 13 + .../icons/icon_godot_ranger3d.svg.import | 37 + addons/func_godot/icons/icon_quake_file.svg | 85 ++ .../icons/icon_quake_file.svg.import | 37 + addons/func_godot/icons/icon_slipgate3d.svg | 13 + .../icons/icon_slipgate3d.svg.import | 37 + addons/func_godot/palette.lmp | Bin 0 -> 768 bytes addons/func_godot/palette.lmp.import | 14 + addons/func_godot/plugin.cfg | 7 + addons/func_godot/src/core/func_godot.gd | 136 ++ addons/func_godot/src/core/func_godot.gd.uid | 1 + .../src/core/func_godot_geo_generator.gd | 381 ++++++ .../src/core/func_godot_geo_generator.gd.uid | 1 + .../src/core/func_godot_map_data.gd | 158 +++ .../src/core/func_godot_map_data.gd.uid | 1 + .../src/core/func_godot_map_parser.gd | 326 +++++ .../src/core/func_godot_map_parser.gd.uid | 1 + .../src/core/func_godot_surface_gatherer.gd | 217 ++++ .../core/func_godot_surface_gatherer.gd.uid | 1 + .../src/fgd/func_godot_fgd_base_class.gd | 7 + .../src/fgd/func_godot_fgd_base_class.gd.uid | 1 + .../src/fgd/func_godot_fgd_entity_class.gd | 217 ++++ .../fgd/func_godot_fgd_entity_class.gd.uid | 1 + .../func_godot/src/fgd/func_godot_fgd_file.gd | 166 +++ .../src/fgd/func_godot_fgd_file.gd.uid | 1 + .../fgd/func_godot_fgd_model_point_class.gd | 166 +++ .../func_godot_fgd_model_point_class.gd.uid | 1 + .../src/fgd/func_godot_fgd_point_class.gd | 23 + .../src/fgd/func_godot_fgd_point_class.gd.uid | 1 + .../src/fgd/func_godot_fgd_solid_class.gd | 88 ++ .../src/fgd/func_godot_fgd_solid_class.gd.uid | 1 + addons/func_godot/src/func_godot_plugin.gd | 186 +++ .../func_godot/src/func_godot_plugin.gd.uid | 1 + .../func_godot/src/import/quake_map_file.gd | 6 + .../src/import/quake_map_file.gd.uid | 1 + .../src/import/quake_map_import_plugin.gd | 47 + .../src/import/quake_map_import_plugin.gd.uid | 1 + .../src/import/quake_palette_file.gd | 8 + .../src/import/quake_palette_file.gd.uid | 1 + .../src/import/quake_palette_import_plugin.gd | 61 + .../import/quake_palette_import_plugin.gd.uid | 1 + .../func_godot/src/import/quake_wad_file.gd | 8 + .../src/import/quake_wad_file.gd.uid | 1 + .../src/import/quake_wad_import_plugin.gd | 209 +++ .../src/import/quake_wad_import_plugin.gd.uid | 1 + addons/func_godot/src/map/func_godot_map.gd | 1138 +++++++++++++++++ .../func_godot/src/map/func_godot_map.gd.uid | 1 + .../src/map/func_godot_map_settings.gd | 85 ++ .../src/map/func_godot_map_settings.gd.uid | 1 + .../netradiant_custom_gamepack_config.gd | 274 ++++ .../netradiant_custom_gamepack_config.gd.uid | 1 + .../netradiant_custom_shader.gd | 10 + .../netradiant_custom_shader.gd.uid | 1 + .../trenchbroom/trenchbroom_game_config.gd | 331 +++++ .../trenchbroom_game_config.gd.uid | 1 + .../src/trenchbroom/trenchbroom_tag.gd | 26 + .../src/trenchbroom/trenchbroom_tag.gd.uid | 1 + .../src/util/func_godot_local_config.gd | 142 ++ .../src/util/func_godot_local_config.gd.uid | 1 + .../src/util/func_godot_texture_loader.gd | 187 +++ .../src/util/func_godot_texture_loader.gd.uid | 1 + addons/func_godot/src/util/func_godot_util.gd | 40 + .../src/util/func_godot_util.gd.uid | 1 + .../func_godot/textures/default_material.tres | 8 + .../func_godot/textures/default_texture.png | 3 + .../textures/default_texture.png.import | 34 + addons/func_godot/textures/special/clip.png | 3 + .../textures/special/clip.png.import | 34 + addons/func_godot/textures/special/origin.png | 3 + .../textures/special/origin.png.import | 34 + addons/func_godot/textures/special/skip.png | 3 + .../textures/special/skip.png.import | 34 + textures/Floors | 1 + textures/Various | 1 + textures/Walls | 1 + 806 files changed, 18966 insertions(+), 15 deletions(-) create mode 100644 3D/Maps/test.map create mode 100644 ExternalMaterial/HexGrid/Singles/.gdignore create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor0.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor0.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor0.tres create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor1.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor1.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor10.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor10.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor100.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor100.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor101.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor101.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor102.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor102.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor103.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor103.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor104.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor104.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor105.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor105.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor106.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor106.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor107.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor107.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor108.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor108.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor109.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor109.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor11.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor11.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor110.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor110.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor111.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor111.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor112.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor112.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor113.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor113.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor114.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor114.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor115.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor115.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor116.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor116.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor117.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor117.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor118.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor118.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor119.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor119.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor12.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor12.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor120.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor120.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor121.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor121.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor122.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor122.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor123.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor123.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor124.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor124.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor125.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor125.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor126.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor126.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor127.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor127.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor128.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor128.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor129.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor129.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor13.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor13.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor130.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor130.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor131.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor131.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor132.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor132.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor133.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor133.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor134.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor134.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor135.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor135.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor136.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor136.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor137.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor137.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor138.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor138.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor139.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor139.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor14.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor14.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor140.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor140.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor141.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor141.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor142.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor142.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor143.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor143.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor144.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor144.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor145.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor145.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor146.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor146.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor147.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor147.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor148.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor148.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor149.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor149.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor15.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor15.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor150.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor150.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor151.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor151.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor152.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor152.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor153.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor153.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor154.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor154.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor155.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor155.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor156.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor156.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor157.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor157.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor158.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor158.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor159.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor159.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor16.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor16.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor160.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor160.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor161.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor161.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor162.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor162.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor163.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor163.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor164.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor164.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor165.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor165.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor166.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor166.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor167.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor167.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor168.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor168.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor169.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor169.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor17.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor17.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor170.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor170.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor171.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor171.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor172.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor172.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor173.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor173.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor174.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor174.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor175.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor175.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor176.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor176.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor177.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor177.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor178.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor178.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor179.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor179.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor18.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor18.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor180.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor180.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor181.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor181.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor182.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor182.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor183.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor183.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor184.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor184.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor185.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor185.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor186.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor186.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor187.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor187.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor188.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor188.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor189.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor189.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor19.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor19.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor190.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor190.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor191.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor191.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor192.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor192.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor193.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor193.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor194.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor194.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor195.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor195.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor196.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor196.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor197.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor197.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor198.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor198.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor199.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor199.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor2.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor2.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor20.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor20.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor200.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor200.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor201.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor201.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor202.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor202.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor203.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor203.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor204.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor204.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor205.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor205.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor206.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor206.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor207.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor207.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor208.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor208.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor209.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor209.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor21.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor21.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor210.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor210.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor211.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor211.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor212.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor212.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor213.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor213.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor214.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor214.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor215.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor215.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor216.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor216.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor217.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor217.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor218.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor218.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor219.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor219.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor22.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor22.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor220.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor220.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor221.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor221.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor222.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor222.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor223.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor223.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor224.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor224.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor225.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor225.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor226.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor226.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor227.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor227.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor228.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor228.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor229.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor229.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor23.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor23.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor230.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor230.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor231.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor231.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor232.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor232.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor233.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor233.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor234.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor234.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor235.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor235.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor236.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor236.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor237.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor237.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor238.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor238.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor239.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor239.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor24.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor24.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor240.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor240.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor241.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor241.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor242.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor242.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor243.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor243.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor244.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor244.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor245.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor245.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor246.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor246.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor247.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor247.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor248.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor248.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor249.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor249.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor25.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor25.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor250.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor250.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor251.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor251.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor252.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor252.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor253.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor253.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor254.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor254.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor255.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor255.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor256.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor256.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor257.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor257.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor258.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor258.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor259.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor259.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor26.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor26.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor260.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor260.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor261.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor261.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor262.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor262.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor263.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor263.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor264.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor264.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor265.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor265.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor266.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor266.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor267.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor267.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor268.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor268.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor269.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor269.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor27.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor27.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor270.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor270.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor271.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor271.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor272.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor272.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor273.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor273.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor274.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor274.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor275.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor275.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor276.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor276.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor277.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor277.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor278.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor278.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor279.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor279.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor28.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor28.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor280.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor280.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor281.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor281.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor282.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor282.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor283.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor283.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor284.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor284.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor285.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor285.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor286.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor286.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor287.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor287.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor288.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor288.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor289.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor289.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor29.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor29.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor290.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor290.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor291.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor291.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor292.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor292.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor293.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor293.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor294.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor294.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor295.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor295.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor296.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor296.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor297.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor297.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor298.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor298.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor299.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor299.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor3.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor3.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor30.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor30.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor300.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor300.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor301.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor301.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor302.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor302.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor303.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor303.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor304.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor304.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor305.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor305.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor306.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor306.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor307.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor307.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor308.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor308.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor309.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor309.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor31.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor31.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor310.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor310.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor311.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor311.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor312.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor312.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor313.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor313.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor314.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor314.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor315.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor315.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor316.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor316.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor317.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor317.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor318.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor318.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor319.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor319.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor32.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor32.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor33.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor33.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor34.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor34.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor35.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor35.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor36.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor36.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor37.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor37.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor38.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor38.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor39.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor39.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor4.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor4.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor40.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor40.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor41.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor41.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor42.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor42.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor43.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor43.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor44.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor44.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor45.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor45.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor46.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor46.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor47.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor47.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor48.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor48.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor49.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor49.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor5.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor5.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor50.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor50.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor51.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor51.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor52.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor52.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor53.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor53.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor54.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor54.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor55.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor55.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor56.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor56.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor57.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor57.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor58.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor58.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor59.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor59.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor6.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor6.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor60.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor60.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor61.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor61.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor62.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor62.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor63.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor63.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor64.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor64.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor65.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor65.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor66.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor66.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor67.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor67.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor68.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor68.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor69.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor69.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor7.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor7.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor70.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor70.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor71.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor71.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor72.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor72.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor73.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor73.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor74.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor74.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor75.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor75.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor76.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor76.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor77.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor77.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor78.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor78.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor79.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor79.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor8.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor8.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor80.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor80.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor81.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor81.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor82.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor82.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor83.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor83.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor84.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor84.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor85.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor85.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor86.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor86.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor87.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor87.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor88.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor88.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor89.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor89.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor9.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor9.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor90.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor90.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor91.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor91.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor92.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor92.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor93.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor93.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor94.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor94.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor95.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor95.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor96.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor96.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor97.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor97.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor98.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor98.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor99.png create mode 100644 ExternalMaterial/HexGrid/Singles/Floors/Floor99.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various0.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various0.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various1.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various1.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various10.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various10.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various11.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various11.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various12.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various12.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various13.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various13.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various14.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various14.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various15.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various15.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various16.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various16.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various17.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various17.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various2.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various2.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various3.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various3.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various4.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various4.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various5.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various5.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various6.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various6.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various7.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various7.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various8.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various8.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various9.png create mode 100644 ExternalMaterial/HexGrid/Singles/Various/Various9.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall0.png create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall0.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall0.tres create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall1.png create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall1.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall2.png create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall2.png.import create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall3.png create mode 100644 ExternalMaterial/HexGrid/Singles/Walls/Wall3.png.import create mode 100644 IsoTest/IsoGrid.cs create mode 100644 IsoTest/IsoGrid.cs.uid create mode 100644 IsoTest/IsoMapTest1.tscn create mode 100644 IsoTest/IsoMapTest2.tscn create mode 100644 IsoTest/movable.png create mode 100644 IsoTest/movable.png.import create mode 100644 addons/func_godot/fgd/func_detail.tres create mode 100644 addons/func_godot/fgd/func_detail_illusionary.tres create mode 100644 addons/func_godot/fgd/func_geo.tres create mode 100644 addons/func_godot/fgd/func_godot_fgd.tres create mode 100644 addons/func_godot/fgd/func_illusionary.tres create mode 100644 addons/func_godot/fgd/phong_base.tres create mode 100644 addons/func_godot/fgd/worldspawn.tres create mode 100644 addons/func_godot/func_godot_default_map_settings.tres create mode 100644 addons/func_godot/func_godot_local_config.tres create mode 100644 addons/func_godot/game_config/netradiant_custom/func_godot_netradiant_custom_gamepack_config.tres create mode 100644 addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres create mode 100644 addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres create mode 100644 addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres create mode 100644 addons/func_godot/game_config/trenchbroom/func_godot_tb_game_config.tres create mode 100644 addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres create mode 100644 addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres create mode 100644 addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres create mode 100644 addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres create mode 100644 addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres create mode 100644 addons/func_godot/icon.png create mode 100644 addons/func_godot/icon.png.import create mode 100644 addons/func_godot/icon.svg create mode 100644 addons/func_godot/icon.svg.import create mode 100644 addons/func_godot/icon32.png create mode 100644 addons/func_godot/icon32.png.import create mode 100644 addons/func_godot/icons/icon_godambler.svg create mode 100644 addons/func_godot/icons/icon_godambler.svg.import create mode 100644 addons/func_godot/icons/icon_godambler3d.svg create mode 100644 addons/func_godot/icons/icon_godambler3d.svg.import create mode 100644 addons/func_godot/icons/icon_godot_ranger.svg create mode 100644 addons/func_godot/icons/icon_godot_ranger.svg.import create mode 100644 addons/func_godot/icons/icon_godot_ranger3d.svg create mode 100644 addons/func_godot/icons/icon_godot_ranger3d.svg.import create mode 100644 addons/func_godot/icons/icon_quake_file.svg create mode 100644 addons/func_godot/icons/icon_quake_file.svg.import create mode 100644 addons/func_godot/icons/icon_slipgate3d.svg create mode 100644 addons/func_godot/icons/icon_slipgate3d.svg.import create mode 100644 addons/func_godot/palette.lmp create mode 100644 addons/func_godot/palette.lmp.import create mode 100644 addons/func_godot/plugin.cfg create mode 100644 addons/func_godot/src/core/func_godot.gd create mode 100644 addons/func_godot/src/core/func_godot.gd.uid create mode 100644 addons/func_godot/src/core/func_godot_geo_generator.gd create mode 100644 addons/func_godot/src/core/func_godot_geo_generator.gd.uid create mode 100644 addons/func_godot/src/core/func_godot_map_data.gd create mode 100644 addons/func_godot/src/core/func_godot_map_data.gd.uid create mode 100644 addons/func_godot/src/core/func_godot_map_parser.gd create mode 100644 addons/func_godot/src/core/func_godot_map_parser.gd.uid create mode 100644 addons/func_godot/src/core/func_godot_surface_gatherer.gd create mode 100644 addons/func_godot/src/core/func_godot_surface_gatherer.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_base_class.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_file.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_point_class.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd create mode 100644 addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid create mode 100644 addons/func_godot/src/func_godot_plugin.gd create mode 100644 addons/func_godot/src/func_godot_plugin.gd.uid create mode 100644 addons/func_godot/src/import/quake_map_file.gd create mode 100644 addons/func_godot/src/import/quake_map_file.gd.uid create mode 100644 addons/func_godot/src/import/quake_map_import_plugin.gd create mode 100644 addons/func_godot/src/import/quake_map_import_plugin.gd.uid create mode 100644 addons/func_godot/src/import/quake_palette_file.gd create mode 100644 addons/func_godot/src/import/quake_palette_file.gd.uid create mode 100644 addons/func_godot/src/import/quake_palette_import_plugin.gd create mode 100644 addons/func_godot/src/import/quake_palette_import_plugin.gd.uid create mode 100644 addons/func_godot/src/import/quake_wad_file.gd create mode 100644 addons/func_godot/src/import/quake_wad_file.gd.uid create mode 100644 addons/func_godot/src/import/quake_wad_import_plugin.gd create mode 100644 addons/func_godot/src/import/quake_wad_import_plugin.gd.uid create mode 100644 addons/func_godot/src/map/func_godot_map.gd create mode 100644 addons/func_godot/src/map/func_godot_map.gd.uid create mode 100644 addons/func_godot/src/map/func_godot_map_settings.gd create mode 100644 addons/func_godot/src/map/func_godot_map_settings.gd.uid create mode 100644 addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd create mode 100644 addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid create mode 100644 addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd create mode 100644 addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid create mode 100644 addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd create mode 100644 addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid create mode 100644 addons/func_godot/src/trenchbroom/trenchbroom_tag.gd create mode 100644 addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid create mode 100644 addons/func_godot/src/util/func_godot_local_config.gd create mode 100644 addons/func_godot/src/util/func_godot_local_config.gd.uid create mode 100644 addons/func_godot/src/util/func_godot_texture_loader.gd create mode 100644 addons/func_godot/src/util/func_godot_texture_loader.gd.uid create mode 100644 addons/func_godot/src/util/func_godot_util.gd create mode 100644 addons/func_godot/src/util/func_godot_util.gd.uid create mode 100644 addons/func_godot/textures/default_material.tres create mode 100644 addons/func_godot/textures/default_texture.png create mode 100644 addons/func_godot/textures/default_texture.png.import create mode 100644 addons/func_godot/textures/special/clip.png create mode 100644 addons/func_godot/textures/special/clip.png.import create mode 100644 addons/func_godot/textures/special/origin.png create mode 100644 addons/func_godot/textures/special/origin.png.import create mode 100644 addons/func_godot/textures/special/skip.png create mode 100644 addons/func_godot/textures/special/skip.png.import create mode 120000 textures/Floors create mode 120000 textures/Various create mode 120000 textures/Walls diff --git a/3D/Maps/test.map b/3D/Maps/test.map new file mode 100644 index 00000000..8a5e57f8 --- /dev/null +++ b/3D/Maps/test.map @@ -0,0 +1,25 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"_tb_textures" "textures" +// brush 0 +{ +( -64 -64 -16 ) ( -64 -63 -16 ) ( -64 -64 -15 ) __TB_empty 0 0 0 1 1 +( -64 -64 -16 ) ( -64 -64 -15 ) ( -63 -64 -16 ) __TB_empty 0 0 0 1 1 +( -64 -64 -16 ) ( -63 -64 -16 ) ( -64 -63 -16 ) __TB_empty 0 0 0 1 1 +( 64 64 16 ) ( 64 65 16 ) ( 65 64 16 ) __TB_empty 0 0 0 1 1 +( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) __TB_empty 0 0 0 1 1 +( 64 64 16 ) ( 64 64 17 ) ( 64 65 16 ) __TB_empty 0 0 0 1 1 +} +// brush 1 +{ +( 64 -32 32 ) ( 64 -31 32 ) ( 64 -32 33 ) __TB_empty 16 0 0 1 1 +( 64 -32 32 ) ( 64 -32 33 ) ( 65 -32 32 ) __TB_empty -16 0 0 1 1 +( 64 -32 32 ) ( 65 -32 32 ) ( 64 -31 32 ) __TB_empty -16 -16 0 1 1 +( 80 16 48 ) ( 80 17 48 ) ( 81 16 48 ) __TB_empty -16 -16 0 1 1 +( 80 16 48 ) ( 81 16 48 ) ( 80 16 49 ) __TB_empty -16 0 0 1 1 +( 80 16 48 ) ( 80 16 49 ) ( 80 17 48 ) __TB_empty 16 0 0 1 1 +} +} diff --git a/ExternalMaterial/Barrel/Barrels.png.import b/ExternalMaterial/Barrel/Barrels.png.import index 5d5ef497..a76ab332 100644 --- a/ExternalMaterial/Barrel/Barrels.png.import +++ b/ExternalMaterial/Barrel/Barrels.png.import @@ -3,25 +3,26 @@ importer="texture" type="CompressedTexture2D" uid="uid://du8xcvbnf30o2" -path="res://.godot/imported/Barrels.png-03ba2819d65a96f6d366c3d96a690e13.ctex" +path.s3tc="res://.godot/imported/Barrels.png-03ba2819d65a96f6d366c3d96a690e13.s3tc.ctex" metadata={ -"vram_texture": false +"imported_formats": ["s3tc_bptc"], +"vram_texture": true } [deps] source_file="res://ExternalMaterial/Barrel/Barrels.png" -dest_files=["res://.godot/imported/Barrels.png-03ba2819d65a96f6d366c3d96a690e13.ctex"] +dest_files=["res://.godot/imported/Barrels.png-03ba2819d65a96f6d366c3d96a690e13.s3tc.ctex"] [params] -compress/mode=0 +compress/mode=2 compress/high_quality=false compress/lossy_quality=0.7 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=false +mipmaps/generate=true mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" @@ -31,4 +32,4 @@ process/normal_map_invert_y=false process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 -detect_3d/compress_to=1 +detect_3d/compress_to=0 diff --git a/ExternalMaterial/HexGrid/HexGrid.pxc b/ExternalMaterial/HexGrid/HexGrid.pxc index 4f4be52f27fce6089a4f87b34ebc736fc4e4ddbc..9d5e3967c9c2bc033528262c7728152a50c480f0 100644 GIT binary patch literal 33150 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 literal 27708 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 G_x}L$v8itW diff --git a/ExternalMaterial/HexGrid/HexGrid.pxc1 b/ExternalMaterial/HexGrid/HexGrid.pxc1 index 6ee2b1c5f6849ca8b58087749a947ffdcea18870..4683d9b4ad391ebe9606e2e9899334961eb740df 100644 GIT binary patch literal 33029 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 literal 27511 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≫Z@13WT0F8-Ry1jZ*BQsInbPb(!3;o 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 + + + + + + diff --git a/addons/func_godot/icon.svg.import b/addons/func_godot/icon.svg.import new file mode 100644 index 00000000..e3d1d876 --- /dev/null +++ b/addons/func_godot/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bx5buvf1ydm7q" +path="res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icon.svg" +dest_files=["res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icon32.png b/addons/func_godot/icon32.png new file mode 100644 index 00000000..48bb881b --- /dev/null +++ b/addons/func_godot/icon32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e0fe6df2c51770780378938370f23574a31fca93fe15ac1a394f7478beef491 +size 5356 diff --git a/addons/func_godot/icon32.png.import b/addons/func_godot/icon32.png.import new file mode 100644 index 00000000..e2db946e --- /dev/null +++ b/addons/func_godot/icon32.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://decwujsyhj0qy" +path="res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icon32.png" +dest_files=["res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/func_godot/icons/icon_godambler.svg b/addons/func_godot/icons/icon_godambler.svg new file mode 100644 index 00000000..39b832f3 --- /dev/null +++ b/addons/func_godot/icons/icon_godambler.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/addons/func_godot/icons/icon_godambler.svg.import b/addons/func_godot/icons/icon_godambler.svg.import new file mode 100644 index 00000000..f45a950b --- /dev/null +++ b/addons/func_godot/icons/icon_godambler.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm2kwpq18quv0" +path="res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_godambler.svg" +dest_files=["res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icons/icon_godambler3d.svg b/addons/func_godot/icons/icon_godambler3d.svg new file mode 100644 index 00000000..c40b606f --- /dev/null +++ b/addons/func_godot/icons/icon_godambler3d.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/func_godot/icons/icon_godambler3d.svg.import b/addons/func_godot/icons/icon_godambler3d.svg.import new file mode 100644 index 00000000..2c412153 --- /dev/null +++ b/addons/func_godot/icons/icon_godambler3d.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dieefivfbkovw" +path="res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_godambler3d.svg" +dest_files=["res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icons/icon_godot_ranger.svg b/addons/func_godot/icons/icon_godot_ranger.svg new file mode 100644 index 00000000..80a3aecb --- /dev/null +++ b/addons/func_godot/icons/icon_godot_ranger.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/addons/func_godot/icons/icon_godot_ranger.svg.import b/addons/func_godot/icons/icon_godot_ranger.svg.import new file mode 100644 index 00000000..4bf14235 --- /dev/null +++ b/addons/func_godot/icons/icon_godot_ranger.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfxlhjsefleff" +path="res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_godot_ranger.svg" +dest_files=["res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icons/icon_godot_ranger3d.svg b/addons/func_godot/icons/icon_godot_ranger3d.svg new file mode 100644 index 00000000..5c5aee08 --- /dev/null +++ b/addons/func_godot/icons/icon_godot_ranger3d.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/addons/func_godot/icons/icon_godot_ranger3d.svg.import b/addons/func_godot/icons/icon_godot_ranger3d.svg.import new file mode 100644 index 00000000..e2eb929c --- /dev/null +++ b/addons/func_godot/icons/icon_godot_ranger3d.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://brm515f5ivx8m" +path="res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_godot_ranger3d.svg" +dest_files=["res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icons/icon_quake_file.svg b/addons/func_godot/icons/icon_quake_file.svg new file mode 100644 index 00000000..67383b28 --- /dev/null +++ b/addons/func_godot/icons/icon_quake_file.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/func_godot/icons/icon_quake_file.svg.import b/addons/func_godot/icons/icon_quake_file.svg.import new file mode 100644 index 00000000..b117155e --- /dev/null +++ b/addons/func_godot/icons/icon_quake_file.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0464gp8lby0w" +path="res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_quake_file.svg" +dest_files=["res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/icons/icon_slipgate3d.svg b/addons/func_godot/icons/icon_slipgate3d.svg new file mode 100644 index 00000000..e8c25be1 --- /dev/null +++ b/addons/func_godot/icons/icon_slipgate3d.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/addons/func_godot/icons/icon_slipgate3d.svg.import b/addons/func_godot/icons/icon_slipgate3d.svg.import new file mode 100644 index 00000000..a962587c --- /dev/null +++ b/addons/func_godot/icons/icon_slipgate3d.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfvririkaa4tv" +path="res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/icons/icon_slipgate3d.svg" +dest_files=["res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/func_godot/palette.lmp b/addons/func_godot/palette.lmp new file mode 100644 index 0000000000000000000000000000000000000000..7eefda1dae07f1db534e11a6f7f32d85e01324c6 GIT binary patch literal 768 zcmYjP9hB=p6uvevFfb4p7#Ii)3Glo_?`Rx0stX|bFP$%qR1Fi6oqrHX_~I<4u``yj@RpT znx@C&@$>UTF!BiI9`+egf@XU`!}wjQPGFEDTzk zD9V#$m8Nx3v}M^j*VpyXH0RCc+I15EAOsi#rN9^n0a7BRkk%-QP?8{HaF*esz-5V@ zBWTeN-~n&|A3yX4lX-2_Of;Nee*taUoZXG4ac_M*V}H@v_-YfN@sE#nOw(4>DX7A3+XXI zskcOl(6v6E-EOd(K3{jKYmKer%!S4VQ6_aFWXOg35C6Ddrn* zl~Pv9REUT%MJZn#OMno-Ag~B30Q1qkZ1if8M)O6J8Gb k^{&6a^7b~5-}A>jF9YZRUI0E8`sIH2^Pkswo{vTL7n(ono&W#< literal 0 HcmV?d00001 diff --git a/addons/func_godot/palette.lmp.import b/addons/func_godot/palette.lmp.import new file mode 100644 index 00000000..27c6b0d8 --- /dev/null +++ b/addons/func_godot/palette.lmp.import @@ -0,0 +1,14 @@ +[remap] + +importer="func_godot.palette" +type="Resource" +uid="uid://drgnc41yfybr" +path="res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres" + +[deps] + +source_file="res://addons/func_godot/palette.lmp" +dest_files=["res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres"] + +[params] + diff --git a/addons/func_godot/plugin.cfg b/addons/func_godot/plugin.cfg new file mode 100644 index 00000000..329d1883 --- /dev/null +++ b/addons/func_godot/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="FuncGodot" +description="Quake .map file support for Godot." +author="Shifty, Hannah Crawford, Emberlynn Bland, Tim Maccabe" +version="2025.1" +script="src/func_godot_plugin.gd" diff --git a/addons/func_godot/src/core/func_godot.gd b/addons/func_godot/src/core/func_godot.gd new file mode 100644 index 00000000..eb3096ac --- /dev/null +++ b/addons/func_godot/src/core/func_godot.gd @@ -0,0 +1,136 @@ +class_name FuncGodot extends RefCounted + +var map_data:= FuncGodotMapData.new() +var map_parser:= FuncGodotMapParser.new(map_data) +var geo_generator = preload("res://addons/func_godot/src/core/func_godot_geo_generator.gd").new(map_data) +var map_settings: FuncGodotMapSettings = null: + set(new): + if not new or new == map_settings: return + surface_gatherer.map_settings = new + map_settings = new +var surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data, map_settings) + +func load_map(filename: String, keep_tb_groups: bool) -> void: + map_parser.load_map(filename, keep_tb_groups) + +func get_texture_list() -> PackedStringArray: + var g_textures: PackedStringArray + var tex_count: int = map_data.textures.size() + + g_textures.resize(tex_count) + for i in range(tex_count): + g_textures.set(i, map_data.textures[i].name) + + return g_textures + +func set_entity_definitions(entity_defs: Dictionary) -> void: + for i in range(entity_defs.size()): + var classname: String = entity_defs.keys()[i] + var spawn_type: int = entity_defs.values()[i].get("spawn_type", FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY) + var origin_type: int = entity_defs.values()[i].get("origin_type", FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER) + var metadata_inclusion_flags: int = entity_defs.values()[i].get("metadata_inclusion_flags", FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.NONE) + map_data.set_entity_types_by_classname(classname, spawn_type, origin_type, metadata_inclusion_flags) + +func get_texture_info(texture_name: String) -> FuncGodotMapData.FuncGodotTextureType: + if texture_name == map_settings.origin_texture: + return FuncGodotMapData.FuncGodotTextureType.ORIGIN + return FuncGodotMapData.FuncGodotTextureType.NORMAL + +func generate_geometry(texture_dict: Dictionary) -> void: + var keys: Array = texture_dict.keys() + for key in keys: + var val: Vector2 = texture_dict[key] + map_data.set_texture_info(key, val.x, val.y, get_texture_info(key)) + geo_generator.run() + +func get_entity_dicts() -> Array: + var ent_dicts: Array + for entity in map_data.entities: + var dict: Dictionary + dict["brush_count"] = entity.brushes.size() + + # TODO: This is a horrible remnant of the worldspawn layer system, remove it. + var brush_indices: PackedInt64Array + brush_indices.resize(entity.brushes.size()) + for b in range(entity.brushes.size()): + brush_indices[b] = b + + dict["brush_indices"] = brush_indices + dict["center"] = Vector3(entity.center.y, entity.center.z, entity.center.x) + dict["properties"] = entity.properties + + ent_dicts.append(dict) + + return ent_dicts + +func gather_texture_surfaces(texture_name: String) -> Dictionary: + var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data, map_settings) + sg.reset_params() + sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY + const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags + sg.metadata_skip_flags = MFlags.TEXTURES | MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP + sg.set_texture_filter(texture_name) + sg.set_clip_filter_texture(map_settings.clip_texture) + sg.set_skip_filter_texture(map_settings.skip_texture) + sg.set_origin_filter_texture(map_settings.origin_texture) + sg.run() + return { + surfaces = fetch_surfaces(sg), + metadata = sg.out_metadata, + } + +func gather_entity_convex_collision_surfaces(entity_idx: int) -> void: + surface_gatherer.reset_params() + surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.BRUSH + surface_gatherer.entity_filter_idx = entity_idx + surface_gatherer.set_origin_filter_texture(map_settings.origin_texture) + surface_gatherer.run() + +func gather_entity_concave_collision_surfaces(entity_idx: int) -> void: + surface_gatherer.reset_params() + surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE + surface_gatherer.entity_filter_idx = entity_idx + const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags + surface_gatherer.metadata_skip_flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP + surface_gatherer.set_skip_filter_texture(map_settings.skip_texture) + surface_gatherer.set_origin_filter_texture(map_settings.origin_texture) + surface_gatherer.run() + +func fetch_surfaces(sg: FuncGodotSurfaceGatherer) -> Array: + var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = sg.out_surfaces + var surf_array: Array + + for surf in surfs: + if surf == null or surf.vertices.size() == 0: + surf_array.append(null) + continue + + var vertices: PackedVector3Array + var normals: PackedVector3Array + var tangents: PackedFloat64Array + var uvs: PackedVector2Array + for v in surf.vertices: + vertices.append(Vector3(v.vertex.y, v.vertex.z, v.vertex.x) * map_settings.scale_factor) + normals.append(Vector3(v.normal.y, v.normal.z, v.normal.x)) + tangents.append(v.tangent.y) + tangents.append(v.tangent.z) + tangents.append(v.tangent.x) + tangents.append(v.tangent.w) + uvs.append(Vector2(v.uv.x, v.uv.y)) + + var indices: PackedInt32Array + if surf.indicies.size() > 0: + indices.append_array(surf.indicies) + + var brush_array: Array + brush_array.resize(Mesh.ARRAY_MAX) + + brush_array[Mesh.ARRAY_VERTEX] = vertices + brush_array[Mesh.ARRAY_NORMAL] = normals + brush_array[Mesh.ARRAY_TANGENT] = tangents + brush_array[Mesh.ARRAY_TEX_UV] = uvs + brush_array[Mesh.ARRAY_INDEX] = indices + + surf_array.append(brush_array) + + return surf_array diff --git a/addons/func_godot/src/core/func_godot.gd.uid b/addons/func_godot/src/core/func_godot.gd.uid new file mode 100644 index 00000000..5297e84e --- /dev/null +++ b/addons/func_godot/src/core/func_godot.gd.uid @@ -0,0 +1 @@ +uid://bvstd30rkrap diff --git a/addons/func_godot/src/core/func_godot_geo_generator.gd b/addons/func_godot/src/core/func_godot_geo_generator.gd new file mode 100644 index 00000000..7f0704cb --- /dev/null +++ b/addons/func_godot/src/core/func_godot_geo_generator.gd @@ -0,0 +1,381 @@ +extends RefCounted + +# Min distance between two verts in a brush before they're merged. Higher values fix angled brushes near extents. +const CMP_EPSILON:= 0.008 + +const UP_VECTOR:= Vector3(0.0, 0.0, 1.0) +const RIGHT_VECTOR:= Vector3(0.0, 1.0, 0.0) +const FORWARD_VECTOR:= Vector3(1.0, 0.0, 0.0) + +var map_data: FuncGodotMapData + +var wind_entity_idx: int = 0 +var wind_brush_idx: int = 0 +var wind_face_idx: int = 0 +var wind_face_center: Vector3 +var wind_face_basis: Vector3 +var wind_face_normal: Vector3 + +func _init(in_map_data: FuncGodotMapData) -> void: + map_data = in_map_data + +func sort_vertices_by_winding(a: FuncGodotMapData.FuncGodotFaceVertex, b: FuncGodotMapData.FuncGodotFaceVertex) -> bool: + var face: FuncGodotMapData.FuncGodotFace = map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx] + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx] + + var u: Vector3 = wind_face_basis.normalized() + var v: Vector3 = u.cross(wind_face_normal).normalized() + + var loc_a: Vector3 = a.vertex - wind_face_center + var a_pu: float = loc_a.dot(u) + var a_pv: float = loc_a.dot(v) + + var loc_b: Vector3 = b.vertex - wind_face_center + var b_pu: float = loc_b.dot(u) + var b_pv: float = loc_b.dot(v) + + var a_angle: float = atan2(a_pv, a_pu) + var b_angle: float = atan2(b_pv, b_pu) + + return a_angle < b_angle + +# returns null if no intersection, else intersection vertex. +func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace) -> Variant: + var n0:= f0.plane_normal + var n1:= f1.plane_normal + var n2:= f2.plane_normal + var denom: float = n0.cross(n1).dot(n2) + if denom > 0.0: + return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom + return null + +func vertex_in_hull(faces: Array[FuncGodotMapData.FuncGodotFace], vertex: Vector3) -> bool: + for face in faces: + var proj: float = face.plane_normal.dot(vertex) + if proj > face.plane_dist and absf(face.plane_dist - proj) > CMP_EPSILON: + return false + return true + +func get_standard_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2: + var uv_out: Vector2 + var du:= absf(face.plane_normal.dot(UP_VECTOR)) + var dr:= absf(face.plane_normal.dot(RIGHT_VECTOR)) + var df:= absf(face.plane_normal.dot(FORWARD_VECTOR)) + + if du >= dr and du >= df: + uv_out = Vector2(vertex.x, -vertex.y) + elif dr >= du and dr >= df: + uv_out = Vector2(vertex.x, -vertex.z) + elif df >= du and df >= dr: + uv_out = Vector2(vertex.y, -vertex.z) + + var angle: float = deg_to_rad(face.uv_extra.rot) + uv_out = Vector2( + uv_out.x * cos(angle) - uv_out.y * sin(angle), + uv_out.x * sin(angle) + uv_out.y * cos(angle)) + + uv_out.x /= texture_width + uv_out.y /= texture_height + + uv_out.x /= face.uv_extra.scale_x + uv_out.y /= face.uv_extra.scale_y + + uv_out.x += face.uv_standard.x / texture_width + uv_out.y += face.uv_standard.y / texture_height + + return uv_out + +func get_valve_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2: + var uv_out: Vector2 + var u_axis:= face.uv_valve.u.axis + var v_axis:= face.uv_valve.v.axis + var u_shift:= face.uv_valve.u.offset + var v_shift:= face.uv_valve.v.offset + + uv_out.x = u_axis.dot(vertex); + uv_out.y = v_axis.dot(vertex); + + uv_out.x /= texture_width; + uv_out.y /= texture_height; + + uv_out.x /= face.uv_extra.scale_x; + uv_out.y /= face.uv_extra.scale_y; + + uv_out.x += u_shift / texture_width; + uv_out.y += v_shift / texture_height; + + return uv_out + +func get_standard_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4: + var du:= face.plane_normal.dot(UP_VECTOR) + var dr:= face.plane_normal.dot(RIGHT_VECTOR) + var df:= face.plane_normal.dot(FORWARD_VECTOR) + var dua:= absf(du) + var dra:= absf(dr) + var dfa:= absf(df) + + var u_axis: Vector3 + var v_sign: float = 0.0 + + if dua >= dra and dua >= dfa: + u_axis = FORWARD_VECTOR + v_sign = signf(du) + elif dra >= dua and dra >= dfa: + u_axis = FORWARD_VECTOR + v_sign = -signf(dr) + elif dfa >= dua and dfa >= dra: + u_axis = RIGHT_VECTOR + v_sign = signf(df) + + v_sign *= signf(face.uv_extra.scale_y); + u_axis = u_axis.rotated(face.plane_normal, deg_to_rad(-face.uv_extra.rot) * v_sign) + + return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign) + +func get_valve_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4: + var u_axis:= face.uv_valve.u.axis.normalized() + var v_axis:= face.uv_valve.v.axis.normalized() + var v_sign = -signf(face.plane_normal.cross(u_axis).dot(v_axis)) + return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign) + +func generate_brush_vertices(entity_idx: int, brush_idx: int) -> void: + var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[entity_idx] + var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[brush_idx] + var face_count: int = brush.faces.size() + + var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[entity_idx] + var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[brush_idx] + + var phong: bool = entity.properties.get("_phong", "0") == "1" + var phong_angle_str: String = entity.properties.get("_phong_angle", "89") + var phong_angle: float = float(phong_angle_str) if phong_angle_str.is_valid_float() else 89.0 + + for f0 in range(face_count): + var face: FuncGodotMapData.FuncGodotFace = brush.faces[f0] + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f0] + var texture: FuncGodotMapData.FuncGodotTextureData = map_data.textures[face.texture_idx] + + for f1 in range(face_count): + for f2 in range(face_count): + var vertex = intersect_face(brush.faces[f0], brush.faces[f1], brush.faces[f2]) + if not vertex is Vector3: + continue + if not vertex_in_hull(brush.faces, vertex): + continue + + var merged: bool = false + for f3 in range(f0): + var other_face_geo : FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f3] + for i in range(len(other_face_geo.vertices)): + if other_face_geo.vertices[i].vertex.distance_to(vertex) < CMP_EPSILON: + vertex = other_face_geo.vertices[i].vertex + merged = true; + break + + if merged: + break + + var normal: Vector3 = face.plane_normal + if phong: + var threshold:= cos((phong_angle + 0.01) * 0.0174533) + if face.plane_normal.dot(brush.faces[f1].plane_normal) > threshold: + normal += brush.faces[f1].plane_normal + if face.plane_normal.dot(brush.faces[f2].plane_normal) > threshold: + normal += brush.faces[f2].plane_normal + normal = normal.normalized() + + var uv: Vector2 + var tangent: Vector4 + if face.is_valve_uv: + uv = get_valve_uv(vertex, face, texture.width, texture.height) + tangent = get_valve_tangent(face) + else: + uv = get_standard_uv(vertex, face, texture.width, texture.height) + tangent = get_standard_tangent(face) + + # Check for a duplicate vertex in the current face. + var duplicate_idx: int = -1 + for i in range(face_geo.vertices.size()): + if face_geo.vertices[i].vertex == vertex: + duplicate_idx = i + break + + if duplicate_idx < 0: + var new_face_vert:= FuncGodotMapData.FuncGodotFaceVertex.new() + new_face_vert.vertex = vertex + new_face_vert.normal = normal + new_face_vert.tangent = tangent + new_face_vert.uv = uv + face_geo.vertices.append(new_face_vert) + elif phong: + face_geo.vertices[duplicate_idx].normal += normal + + # maybe optimisable? + for face_geo in brush_geo.faces: + for i in range(face_geo.vertices.size()): + face_geo.vertices[i].normal = face_geo.vertices[i].normal.normalized() + +func run() -> void: + map_data.entity_geo.resize(map_data.entities.size()) + for i in range(map_data.entity_geo.size()): + map_data.entity_geo[i] = FuncGodotMapData.FuncGodotEntityGeometry.new() + + for e in range(map_data.entities.size()): + var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e] + var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e] + entity_geo.brushes.resize(entity.brushes.size()) + for i in range(entity_geo.brushes.size()): + entity_geo.brushes[i] = FuncGodotMapData.FuncGodotBrushGeometry.new() + + for b in range(entity.brushes.size()): + var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b] + var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b] + brush_geo.faces.resize(brush.faces.size()) + for i in range(brush_geo.faces.size()): + brush_geo.faces[i] = FuncGodotMapData.FuncGodotFaceGeometry.new() + + var generate_vertices_task = func(e): + var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e] + var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e] + var entity_mins: Vector3 = Vector3.INF + var entity_maxs: Vector3 = Vector3.INF + var origin_mins: Vector3 = Vector3.INF + var origin_maxs: Vector3 = -Vector3.INF + + entity.center = Vector3.ZERO + + for b in range(entity.brushes.size()): + var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b] + brush.center = Vector3.ZERO + var vert_count: int = 0 + + # Check if this is a special brush (eg: origin) + var brush_texture_type: FuncGodotMapData.FuncGodotTextureType = FuncGodotMapData.FuncGodotTextureType.NORMAL + if brush.faces.size() > 0: + brush_texture_type = map_data.textures[brush.faces[0].texture_idx].type + + # Check that all the faces match the same type + for face_idx in range(1,brush.faces.size()): + if map_data.textures[brush.faces[face_idx].texture_idx].type != brush_texture_type: + brush_texture_type = FuncGodotMapData.FuncGodotTextureType.NORMAL # Reset face type if it doesn't match + break + + generate_brush_vertices(e, b) + + var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = map_data.entity_geo[e].brushes[b] + for face in brush_geo.faces: + for vert in face.vertices: + if entity_mins != Vector3.INF: + entity_mins = entity_mins.min(vert.vertex) + else: + entity_mins = vert.vertex + if entity_maxs != Vector3.INF: + entity_maxs = entity_maxs.max(vert.vertex) + else: + entity_maxs = vert.vertex + + if brush_texture_type == FuncGodotMapData.FuncGodotTextureType.ORIGIN: + if origin_mins != Vector3.INF: + origin_mins = origin_mins.min(vert.vertex) + else: + origin_mins = vert.vertex + if origin_maxs != Vector3.INF: + origin_maxs = origin_maxs.max(vert.vertex) + else: + origin_maxs = vert.vertex + + brush.center += vert.vertex + vert_count += 1 + + if vert_count > 0: + brush.center /= float(vert_count) + + # Default origin type is BOUNDS_CENTER + if entity_maxs != Vector3.INF and entity_mins != Vector3.INF: + entity.center = entity_maxs - ((entity_maxs - entity_mins) * 0.5) + + if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER and entity.brushes.size() > 0: + match entity.origin_type: + FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE, FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE: + if 'origin' in entity.properties: + var origin_comps: PackedFloat64Array = entity.properties['origin'].split_floats(' ') + if origin_comps.size() > 2: + if entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE: + entity.center = Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) + else: # OriginType.RELATIVE + entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) + + FuncGodotMapData.FuncGodotEntityOriginType.BRUSH: + if origin_mins != Vector3.INF: + entity.center = origin_maxs - ((origin_maxs - origin_mins) * 0.5) + + FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MINS: + entity.center = entity_mins + + FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MAXS: + entity.center = entity_maxs + + FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED: + entity.center = Vector3.ZERO + for b in range(entity.brushes.size()): + entity.center += entity.brushes[b].center + entity.center /= float(entity.brushes.size()) + + var generate_vertices_task_id:= WorkerThreadPool.add_group_task(generate_vertices_task, map_data.entities.size(), 4, true) + WorkerThreadPool.wait_for_group_task_completion(generate_vertices_task_id) + + # wind face vertices + for e in range(map_data.entities.size()): + var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e] + var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e] + + for b in range(entity.brushes.size()): + var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b] + var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b] + + for f in range(brush.faces.size()): + var face: FuncGodotMapData.FuncGodotFace = brush.faces[f] + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f] + + if face_geo.vertices.size() < 3: + continue + + wind_entity_idx = e + wind_brush_idx = b + wind_face_idx = f + + wind_face_basis = face_geo.vertices[1].vertex - face_geo.vertices[0].vertex + wind_face_center = Vector3.ZERO + wind_face_normal = face.plane_normal + + for v in face_geo.vertices: + wind_face_center += v.vertex + + wind_face_center /= face_geo.vertices.size() + + face_geo.vertices.sort_custom(sort_vertices_by_winding) + wind_entity_idx = 0 + + # index face vertices + var index_faces_task:= func(e): + var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e] + + for b in range(entity_geo.brushes.size()): + var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b] + + for f in range(brush_geo.faces.size()): + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f] + + if face_geo.vertices.size() < 3: + continue + + var i_count: int = 0 + face_geo.indicies.resize((face_geo.vertices.size() - 2) * 3) + for i in range(face_geo.vertices.size() - 2): + face_geo.indicies[i_count] = 0 + face_geo.indicies[i_count + 1] = i + 1 + face_geo.indicies[i_count + 2] = i + 2 + i_count += 3 + + var index_faces_task_id:= WorkerThreadPool.add_group_task(index_faces_task, map_data.entities.size(), 4, true) + WorkerThreadPool.wait_for_group_task_completion(index_faces_task_id) diff --git a/addons/func_godot/src/core/func_godot_geo_generator.gd.uid b/addons/func_godot/src/core/func_godot_geo_generator.gd.uid new file mode 100644 index 00000000..f9f290ec --- /dev/null +++ b/addons/func_godot/src/core/func_godot_geo_generator.gd.uid @@ -0,0 +1 @@ +uid://cb0c2fn35hqov diff --git a/addons/func_godot/src/core/func_godot_map_data.gd b/addons/func_godot/src/core/func_godot_map_data.gd new file mode 100644 index 00000000..1b6137f1 --- /dev/null +++ b/addons/func_godot/src/core/func_godot_map_data.gd @@ -0,0 +1,158 @@ +class_name FuncGodotMapData extends RefCounted + +var entities: Array[FuncGodotMapData.FuncGodotEntity] +var entity_geo: Array[FuncGodotMapData.FuncGodotEntityGeometry] +var textures: Array[FuncGodotMapData.FuncGodotTextureData] + +func register_texture(name: String) -> int: + for i in range(textures.size()): + if textures[i].name == name: + return i + + textures.append(FuncGodotTextureData.new(name)) + return textures.size() - 1 + +func set_texture_info(name: String, width: int, height: int, type: FuncGodotTextureType) -> void: + for i in range(textures.size()): + if textures[i].name == name: + textures[i].width = width + textures[i].height = height + textures[i].type = type + return + +func find_texture(texture_name: String) -> int: + for i in range(textures.size()): + if textures[i].name == texture_name: + return i + return -1 + +func set_entity_types_by_classname(classname: String, spawn_type: int, origin_type: int, meta_flags: int) -> void: + for entity in entities: + if entity.properties.has("classname") and entity.properties["classname"] == classname: + entity.metadata_inclusion_flags = meta_flags as FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags + entity.spawn_type = spawn_type as FuncGodotMapData.FuncGodotEntitySpawnType + if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY: + entity.origin_type = origin_type as FuncGodotMapData.FuncGodotEntityOriginType + else: + entity.origin_type = FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED + +func clear() -> void: + entities.clear() + entity_geo.clear() + textures.clear() + +# -------------------------------------------------------------------------------------------------- +# Nested Types +# -------------------------------------------------------------------------------------------------- +enum FuncGodotEntitySpawnType { + WORLDSPAWN = 0, + MERGE_WORLDSPAWN = 1, + ENTITY = 2 +} + +enum FuncGodotEntityOriginType { + AVERAGED = 0, + ABSOLUTE = 1, + RELATIVE = 2, + BRUSH = 3, + BOUNDS_CENTER = 4, + BOUNDS_MINS = 5, + BOUNDS_MAXS = 6, +} + +enum FuncGodotEntityMetadataInclusionFlags { + NONE = 0, + ENTITY_INDEX_RANGES = 1, + TEXTURES = 2, + VERTEX = 4, + FACE_POSITION = 8, + FACE_NORMAL = 16, + COLLISION_SHAPE_TO_FACE_RANGE_MAP = 32, +} + +enum FuncGodotTextureType { + NORMAL = 0, + ORIGIN = 1 +} + +class FuncGodotFacePoints: + var v0: Vector3 + var v1: Vector3 + var v2: Vector3 + +class FuncGodotValveTextureAxis: + var axis: Vector3 + var offset: float + +class FuncGodotValveUV: + var u: FuncGodotValveTextureAxis + var v: FuncGodotValveTextureAxis + + func _init() -> void: + u = FuncGodotValveTextureAxis.new() + v = FuncGodotValveTextureAxis.new() + +class FuncGodotFaceUVExtra: + var rot: float + var scale_x: float + var scale_y: float + +class FuncGodotFace: + var plane_points: FuncGodotFacePoints + var plane_normal: Vector3 + var plane_dist: float + var texture_idx: int + var is_valve_uv: bool + var uv_standard: Vector2 + var uv_valve: FuncGodotValveUV + var uv_extra: FuncGodotFaceUVExtra + + func _init() -> void: + plane_points = FuncGodotFacePoints.new() + uv_valve = FuncGodotValveUV.new() + uv_extra = FuncGodotFaceUVExtra.new() + +class FuncGodotBrush: + var faces: Array[FuncGodotFace] + var center: Vector3 + +class FuncGodotEntity: + var properties: Dictionary + var brushes: Array[FuncGodotBrush] + var center: Vector3 + var spawn_type: FuncGodotEntitySpawnType + var origin_type: FuncGodotEntityOriginType + var metadata_inclusion_flags: FuncGodotEntityMetadataInclusionFlags + +class FuncGodotFaceVertex: + var vertex: Vector3 + var normal: Vector3 + var uv: Vector2 + var tangent: Vector4 + + func duplicate() -> FuncGodotFaceVertex: + var new_vert := FuncGodotFaceVertex.new() + new_vert.vertex = vertex + new_vert.normal = normal + new_vert.uv = uv + new_vert.tangent = tangent + return new_vert + +class FuncGodotFaceGeometry: + var vertices: Array[FuncGodotFaceVertex] + var indicies: Array[int] + +class FuncGodotBrushGeometry: + var faces: Array[FuncGodotFaceGeometry] + +class FuncGodotEntityGeometry: + var brushes: Array[FuncGodotBrushGeometry] + +class FuncGodotTextureData: + var name: String + var width: int + var height: int + var type: FuncGodotTextureType + + func _init(in_name: String): + name = in_name diff --git a/addons/func_godot/src/core/func_godot_map_data.gd.uid b/addons/func_godot/src/core/func_godot_map_data.gd.uid new file mode 100644 index 00000000..3027e55e --- /dev/null +++ b/addons/func_godot/src/core/func_godot_map_data.gd.uid @@ -0,0 +1 @@ +uid://ct3rx5npjd00s diff --git a/addons/func_godot/src/core/func_godot_map_parser.gd b/addons/func_godot/src/core/func_godot_map_parser.gd new file mode 100644 index 00000000..ed6cc037 --- /dev/null +++ b/addons/func_godot/src/core/func_godot_map_parser.gd @@ -0,0 +1,326 @@ +class_name FuncGodotMapParser extends RefCounted + +var scope:= FuncGodotMapParser.ParseScope.FILE +var comment: bool = false +var entity_idx: int = -1 +var brush_idx: int = -1 +var face_idx: int = -1 +var component_idx: int = 0 +var prop_key: String = "" +var current_property: String = "" +var valve_uvs: bool = false + +var current_face: FuncGodotMapData.FuncGodotFace +var current_brush: FuncGodotMapData.FuncGodotBrush +var current_entity: FuncGodotMapData.FuncGodotEntity + +var map_data: FuncGodotMapData +var _keep_tb_groups: bool = false + +func _init(in_map_data: FuncGodotMapData) -> void: + map_data = in_map_data + +func load_map(map_file: String, keep_tb_groups: bool) -> bool: + current_face = FuncGodotMapData.FuncGodotFace.new() + current_brush = FuncGodotMapData.FuncGodotBrush.new() + current_entity = FuncGodotMapData.FuncGodotEntity.new() + + scope = FuncGodotMapParser.ParseScope.FILE + comment = false + entity_idx = -1 + brush_idx = -1 + face_idx = -1 + component_idx = 0 + valve_uvs = false + _keep_tb_groups = keep_tb_groups + + var lines: PackedStringArray = [] + + var map: FileAccess = FileAccess.open(map_file, FileAccess.READ) + + if map == null: + printerr("Error: Failed to open map file (" + map_file + ")") + return false + + if map_file.ends_with(".import"): + while not map.eof_reached(): + var line: String = map.get_line() + if line.begins_with("path"): + map.close() + line = line.replace("path=", ""); + line = line.replace('"', '') + var map_data: String = (load(line) as QuakeMapFile).map_data + if map_data.is_empty(): + printerr("Error: Failed to open map file (" + line + ")") + return false + lines = map_data.split("\n") + break + else: + while not map.eof_reached(): + var line: String = map.get_line() + lines.append(line) + + for line in lines: + if comment: + comment = false + var tokens := split_string(line, [" ", "\t"], true) + for s in tokens: + token(s) + + return true + +func split_string(s: String, delimeters: Array[String], allow_empty: bool = true) -> Array[String]: + var parts: Array[String] = [] + + var start := 0 + var i := 0 + + while i < s.length(): + if s[i] in delimeters: + if allow_empty or start < i: + parts.push_back(s.substr(start, i - start)) + start = i + 1 + i += 1 + + if allow_empty or start < i: + parts.push_back(s.substr(start, i - start)) + + return parts + +func set_scope(new_scope: FuncGodotMapParser.ParseScope) -> void: + """ + match new_scope: + ParseScope.FILE: + print("Switching to file scope.") + ParseScope.ENTITY: + print("Switching to entity " + str(entity_idx) + "scope") + ParseScope.PROPERTY_VALUE: + print("Switching to property value scope") + ParseScope.BRUSH: + print("Switching to brush " + str(brush_idx) + " scope") + ParseScope.PLANE_0: + print("Switching to face " + str(face_idx) + " plane 0 scope") + ParseScope.PLANE_1: + print("Switching to face " + str(face_idx) + " plane 1 scope") + ParseScope.PLANE_2: + print("Switching to face " + str(face_idx) + " plane 2 scope") + ParseScope.TEXTURE: + print("Switching to texture scope") + ParseScope.U: + print("Switching to U scope") + ParseScope.V: + print("Switching to V scope") + ParseScope.VALVE_U: + print("Switching to Valve U scope") + ParseScope.VALVE_V: + print("Switching to Valve V scope") + ParseScope.ROT: + print("Switching to rotation scope") + ParseScope.U_SCALE: + print("Switching to U scale scope") + ParseScope.V_SCALE: + print("Switching to V scale scope") + """ + scope = new_scope + +func token(buf_str: String) -> void: + if comment: + return + elif buf_str == "//": + comment = true + return + + match scope: + FuncGodotMapParser.ParseScope.FILE: + if buf_str == "{": + entity_idx += 1 + brush_idx = -1 + set_scope(FuncGodotMapParser.ParseScope.ENTITY) + FuncGodotMapParser.ParseScope.ENTITY: + if buf_str.begins_with('"'): + prop_key = buf_str.substr(1) + if prop_key.ends_with('"'): + prop_key = prop_key.left(-1) + set_scope(FuncGodotMapParser.ParseScope.PROPERTY_VALUE) + elif buf_str == "{": + brush_idx += 1 + face_idx = -1 + set_scope(FuncGodotMapParser.ParseScope.BRUSH) + elif buf_str == "}": + commit_entity() + set_scope(FuncGodotMapParser.ParseScope.FILE) + FuncGodotMapParser.ParseScope.PROPERTY_VALUE: + var is_first = buf_str[0] == '"' + var is_last = buf_str.right(1) == '"' + + if is_first: + if current_property != "": + current_property = "" + + if not is_last: + current_property += buf_str + " " + else: + current_property += buf_str + + if is_last: + current_entity.properties[prop_key] = current_property.substr(1, len(current_property) - 2) + set_scope(FuncGodotMapParser.ParseScope.ENTITY) + FuncGodotMapParser.ParseScope.BRUSH: + if buf_str == "(": + face_idx += 1 + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.PLANE_0) + elif buf_str == "}": + commit_brush() + set_scope(FuncGodotMapParser.ParseScope.ENTITY) + FuncGodotMapParser.ParseScope.PLANE_0: + if buf_str == ")": + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.PLANE_1) + else: + match component_idx: + 0: + current_face.plane_points.v0.x = float(buf_str) + 1: + current_face.plane_points.v0.y = float(buf_str) + 2: + current_face.plane_points.v0.z = float(buf_str) + + component_idx += 1 + FuncGodotMapParser.ParseScope.PLANE_1: + if buf_str != "(": + if buf_str == ")": + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.PLANE_2) + else: + match component_idx: + 0: + current_face.plane_points.v1.x = float(buf_str) + 1: + current_face.plane_points.v1.y = float(buf_str) + 2: + current_face.plane_points.v1.z = float(buf_str) + + component_idx += 1 + FuncGodotMapParser.ParseScope.PLANE_2: + if buf_str != "(": + if buf_str == ")": + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.TEXTURE) + else: + match component_idx: + 0: + current_face.plane_points.v2.x = float(buf_str) + 1: + current_face.plane_points.v2.y = float(buf_str) + 2: + current_face.plane_points.v2.z = float(buf_str) + + component_idx += 1 + FuncGodotMapParser.ParseScope.TEXTURE: + current_face.texture_idx = map_data.register_texture(buf_str) + set_scope(FuncGodotMapParser.ParseScope.U) + FuncGodotMapParser.ParseScope.U: + if buf_str == "[": + valve_uvs = true + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.VALVE_U) + else: + valve_uvs = false + current_face.uv_standard.x = float(buf_str) + set_scope(FuncGodotMapParser.ParseScope.V) + FuncGodotMapParser.ParseScope.V: + current_face.uv_standard.y = float(buf_str) + set_scope(FuncGodotMapParser.ParseScope.ROT) + FuncGodotMapParser.ParseScope.VALVE_U: + if buf_str == "]": + component_idx = 0 + set_scope(FuncGodotMapParser.ParseScope.VALVE_V) + else: + match component_idx: + 0: + current_face.uv_valve.u.axis.x = float(buf_str) + 1: + current_face.uv_valve.u.axis.y = float(buf_str) + 2: + current_face.uv_valve.u.axis.z = float(buf_str) + 3: + current_face.uv_valve.u.offset = float(buf_str) + + component_idx += 1 + FuncGodotMapParser.ParseScope.VALVE_V: + if buf_str != "[": + if buf_str == "]": + set_scope(FuncGodotMapParser.ParseScope.ROT) + else: + match component_idx: + 0: + current_face.uv_valve.v.axis.x = float(buf_str) + 1: + current_face.uv_valve.v.axis.y = float(buf_str) + 2: + current_face.uv_valve.v.axis.z = float(buf_str) + 3: + current_face.uv_valve.v.offset = float(buf_str) + + component_idx += 1 + FuncGodotMapParser.ParseScope.ROT: + current_face.uv_extra.rot = float(buf_str) + set_scope(FuncGodotMapParser.ParseScope.U_SCALE) + FuncGodotMapParser.ParseScope.U_SCALE: + current_face.uv_extra.scale_x = float(buf_str) + set_scope(FuncGodotMapParser.ParseScope.V_SCALE) + FuncGodotMapParser.ParseScope.V_SCALE: + current_face.uv_extra.scale_y = float(buf_str) + commit_face() + set_scope(FuncGodotMapParser.ParseScope.BRUSH) + +func commit_entity() -> void: + if current_entity.properties.has('_tb_type') and map_data.entities.size() > 0: + map_data.entities[0].brushes.append_array(current_entity.brushes) + current_entity.brushes.clear() + if !_keep_tb_groups: + current_entity = FuncGodotMapData.FuncGodotEntity.new() + return + + var new_entity:= FuncGodotMapData.FuncGodotEntity.new() + new_entity.spawn_type = FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY + new_entity.properties = current_entity.properties + new_entity.brushes = current_entity.brushes + map_data.entities.append(new_entity) + + current_entity = FuncGodotMapData.FuncGodotEntity.new() + +func commit_brush() -> void: + current_entity.brushes.append(current_brush) + current_brush = FuncGodotMapData.FuncGodotBrush.new() + +func commit_face() -> void: + var v0v1: Vector3 = current_face.plane_points.v1 - current_face.plane_points.v0 + var v1v2: Vector3 = current_face.plane_points.v2 - current_face.plane_points.v1 + current_face.plane_normal = v1v2.cross(v0v1).normalized() + current_face.plane_dist = current_face.plane_normal.dot(current_face.plane_points.v0) + current_face.is_valve_uv = valve_uvs + + current_brush.faces.append(current_face) + current_face = FuncGodotMapData.FuncGodotFace.new() + +# Nested +enum ParseScope{ + FILE, + COMMENT, + ENTITY, + PROPERTY_VALUE, + BRUSH, + PLANE_0, + PLANE_1, + PLANE_2, + TEXTURE, + U, + V, + VALVE_U, + VALVE_V, + ROT, + U_SCALE, + V_SCALE +} diff --git a/addons/func_godot/src/core/func_godot_map_parser.gd.uid b/addons/func_godot/src/core/func_godot_map_parser.gd.uid new file mode 100644 index 00000000..658b3505 --- /dev/null +++ b/addons/func_godot/src/core/func_godot_map_parser.gd.uid @@ -0,0 +1 @@ +uid://cg2iiom3svtw0 diff --git a/addons/func_godot/src/core/func_godot_surface_gatherer.gd b/addons/func_godot/src/core/func_godot_surface_gatherer.gd new file mode 100644 index 00000000..f0724340 --- /dev/null +++ b/addons/func_godot/src/core/func_godot_surface_gatherer.gd @@ -0,0 +1,217 @@ +class_name FuncGodotSurfaceGatherer extends RefCounted + +var map_data: FuncGodotMapData +var map_settings: FuncGodotMapSettings +var split_type: SurfaceSplitType = SurfaceSplitType.NONE +var entity_filter_idx: int = -1 +var texture_filter_idx: int = -1 +var clip_filter_texture_idx: int +var skip_filter_texture_idx: int +var origin_filter_texture_idx: int +var metadata_skip_flags: int + +var out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry] +var out_metadata: Dictionary + +func _init(in_map_data: FuncGodotMapData, in_map_settings: FuncGodotMapSettings) -> void: + map_data = in_map_data + map_settings = in_map_settings + +func set_texture_filter(texture_name: String) -> void: + texture_filter_idx = map_data.find_texture(texture_name) + +func set_clip_filter_texture(texture_name: String) -> void: + clip_filter_texture_idx = map_data.find_texture(texture_name) + +func set_skip_filter_texture(texture_name: String) -> void: + skip_filter_texture_idx = map_data.find_texture(texture_name) + +func set_origin_filter_texture(texture_name: String) -> void: + origin_filter_texture_idx = map_data.find_texture(texture_name) + +func filter_entity(entity_idx: int) -> bool: + if entity_filter_idx != -1 and entity_idx != entity_filter_idx: + return true + return false + +func filter_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool: + var face: FuncGodotMapData.FuncGodotFace = map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx] + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx] + + if face_geo.vertices.size() < 3: + return true + + # Omit faces textured with Clip + if clip_filter_texture_idx != -1 and face.texture_idx == clip_filter_texture_idx: + return true + + # Omit faces textured with Skip + if skip_filter_texture_idx != -1 and face.texture_idx == skip_filter_texture_idx: + return true + + # Omit faces textured with Origin + if origin_filter_texture_idx != -1 and face.texture_idx == origin_filter_texture_idx: + return true + + # Omit filtered texture indices + if texture_filter_idx != -1 and face.texture_idx != texture_filter_idx: + return true + + return false + +func run() -> void: + out_surfaces.clear() + var texture_names: Array[StringName] = [] + var textures: PackedInt32Array = [] + var vertices: PackedVector3Array = [] + var positions: PackedVector3Array = [] + var normals: PackedVector3Array = [] + var shape_index_ranges: Array[Vector2i] = [] + var entity_index_ranges: Array[Vector2i] = [] + + var index_offset: int = 0 + var entity_face_range: Vector2i = Vector2i.ZERO + const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags + var build_entity_index_ranges: bool = not metadata_skip_flags & MFlags.ENTITY_INDEX_RANGES + var surf: FuncGodotMapData.FuncGodotFaceGeometry + + if split_type == SurfaceSplitType.NONE: + surf = add_surface() + index_offset = len(out_surfaces) - 1 + + for e in range(map_data.entities.size()): + var entity:= map_data.entities[e] + var entity_geo:= map_data.entity_geo[e] + var shape_face_range := Vector2i.ZERO + var total_entity_tris := 0 + var include_normals_metadata: bool = not metadata_skip_flags & MFlags.FACE_NORMAL and entity.metadata_inclusion_flags & MFlags.FACE_NORMAL + var include_vertices_metadata: bool = not metadata_skip_flags & MFlags.VERTEX and entity.metadata_inclusion_flags & MFlags.VERTEX + var include_textures_metadata: bool = not metadata_skip_flags & MFlags.TEXTURES and entity.metadata_inclusion_flags & MFlags.TEXTURES + var include_positions_metadata: bool = not metadata_skip_flags & MFlags.FACE_POSITION and entity.metadata_inclusion_flags & MFlags.FACE_POSITION + var include_shape_range_metadata: bool = not metadata_skip_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP and entity.metadata_inclusion_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP + + if filter_entity(e): + continue + + if split_type == SurfaceSplitType.ENTITY: + if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.MERGE_WORLDSPAWN: + add_surface() + surf = out_surfaces[0] + index_offset = surf.vertices.size() + else: + surf = add_surface() + index_offset = surf.vertices.size() + + for b in range(entity.brushes.size()): + var brush:= entity.brushes[b] + var brush_geo:= entity_geo.brushes[b] + var total_brush_tris:= 0 + + if split_type == SurfaceSplitType.BRUSH: + index_offset = 0 + surf = add_surface() + + for f in range(brush.faces.size()): + var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f] + var face: FuncGodotMapData.FuncGodotFace = brush.faces[f] + var num_tris = face_geo.vertices.size() - 2 + + if filter_face(e, b, f): + continue + + for v in range(face_geo.vertices.size()): + var vert: FuncGodotMapData.FuncGodotFaceVertex = face_geo.vertices[v].duplicate() + + if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY: + vert.vertex -= entity.center + + surf.vertices.append(vert) + + if include_normals_metadata: + var normal := Vector3(face.plane_normal.y, face.plane_normal.z, face.plane_normal.x) + for i in num_tris: + normals.append(normal) + if include_shape_range_metadata or build_entity_index_ranges: + total_brush_tris += num_tris + if include_textures_metadata: + var texname := StringName(map_data.textures[face.texture_idx].name) + var index: int + if texture_names.is_empty(): + texture_names.append(texname) + index = 0 + elif texture_names.back() == texname: + # Common case, faces with textures are next to each other + index = texture_names.size() - 1 + else: + var texture_name_index: int = texture_names.find(texname) + if texture_name_index == -1: + index = texture_names.size() + texture_names.append(texname) + else: + index = texture_name_index + # Metadata addresses triangles, so we have to duplicate the info for each tri + for i in num_tris: + textures.append(index) + + var avg_vertex_pos := Vector3.ZERO + var avg_vertex_pos_ct: int = 0 + for i in range(num_tris * 3): + surf.indicies.append(face_geo.indicies[i] + index_offset) + var vertex: Vector3 = surf.vertices[surf.indicies.back()].vertex + vertex = Vector3(vertex.y, vertex.z, vertex.x) * map_settings.scale_factor + if include_vertices_metadata: + vertices.append(vertex) + if include_positions_metadata: + avg_vertex_pos_ct += 1 + avg_vertex_pos += vertex + if avg_vertex_pos_ct == 3: + avg_vertex_pos /= 3 + positions.append(avg_vertex_pos) + avg_vertex_pos = Vector3.ZERO + avg_vertex_pos_ct = 0 + + index_offset += face_geo.vertices.size() + + if include_shape_range_metadata: + shape_face_range.x = shape_face_range.y + shape_face_range.y = shape_face_range.x + total_brush_tris + shape_index_ranges.append(shape_face_range) + + if build_entity_index_ranges: + total_entity_tris += total_brush_tris + + if build_entity_index_ranges: + entity_face_range.x = entity_face_range.y + entity_face_range.y = entity_face_range.x + total_entity_tris + entity_index_ranges.append(entity_face_range) + + out_metadata = { + textures = textures, + texture_names = texture_names, + normals = normals, + vertices = vertices, + positions = positions, + shape_index_ranges = shape_index_ranges, + } + if build_entity_index_ranges: + out_metadata["entity_index_ranges"] = entity_index_ranges + +func add_surface() -> FuncGodotMapData.FuncGodotFaceGeometry: + var surf:= FuncGodotMapData.FuncGodotFaceGeometry.new() + out_surfaces.append(surf) + return surf + +func reset_params() -> void: + split_type = SurfaceSplitType.NONE + entity_filter_idx = -1 + texture_filter_idx = -1 + clip_filter_texture_idx = -1 + skip_filter_texture_idx = -1 + metadata_skip_flags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.ENTITY_INDEX_RANGES + +# nested +enum SurfaceSplitType{ + NONE, + ENTITY, + BRUSH +} diff --git a/addons/func_godot/src/core/func_godot_surface_gatherer.gd.uid b/addons/func_godot/src/core/func_godot_surface_gatherer.gd.uid new file mode 100644 index 00000000..3ab162be --- /dev/null +++ b/addons/func_godot/src/core/func_godot_surface_gatherer.gd.uid @@ -0,0 +1 @@ +uid://df8y3hiimomt5 diff --git a/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd b/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd new file mode 100644 index 00000000..c62b1658 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd @@ -0,0 +1,7 @@ +@tool +## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions. Useful for adding shared or common properties and descriptions. +class_name FuncGodotFGDBaseClass +extends FuncGodotFGDEntityClass + +func _init() -> void: + prefix = "@BaseClass" diff --git a/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid new file mode 100644 index 00000000..35726878 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid @@ -0,0 +1 @@ +uid://6o4wbl0iau0v diff --git a/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd b/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd new file mode 100644 index 00000000..36727db7 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd @@ -0,0 +1,217 @@ +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Base entity definition class. Not to be used directly, use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead. +class_name FuncGodotFGDEntityClass +extends Resource + +var prefix: String = "" + +@export_group("Entity Definition") + +## Entity classname. This is a required field in all entity types as it is parsed by both the map editor and by FuncGodot on map build. +@export var classname : String = "" + +## Entity description that appears in the map editor. Not required. +@export_multiline var description : String = "" + +## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process. +@export var func_godot_internal : bool = false + +## FuncGodotFGDBaseClass resources to inherit [member class_properties] and [member class_descriptions] from. +@export var base_classes: Array[Resource] = [] + +## Key value pair properties that will appear in the map editor. After building the FuncGodotMap in Godot, these properties will be added to a Dictionary that gets applied to the generated Node, as long as that Node is a tool script with an exported `func_godot_properties` Dictionary. +@export var class_properties : Dictionary = {} + +## Descriptions for previously defined key value pair properties. +@export var class_property_descriptions : Dictionary = {} + +## Automatically applies entity class properties to matching properties in the generated node. When using this feature, class properties need to be the correct type or you may run into errors on map build. +@export var auto_apply_to_matching_node_properties : bool = false + +## Appearance properties for the map editor. See the [**Valve FGD**](https://developer.valvesoftware.com/wiki/FGD#Entity_Description) and [**TrenchBroom**](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) documentation for more information. +@export var meta_properties : Dictionary = { + "size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)), + "color": Color(0.8, 0.8, 0.8) +} + +@export_group("Node Generation") + +## Node to generate on map build. This can be a built-in Godot class or a GDExtension class. For Point Class entities that use Scene File instantiation leave this blank. +@export var node_class := "" + +## Class property to use in naming the generated node. Overrides `name_property` in [FuncGodotMapSettings]. +## Naming occurs before adding to the [SceneTree] and applying properties. +## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior. +@export var name_property := "" + +func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String: + # Class prefix + var res : String = prefix + + # Meta properties + var base_str = "" + var meta_props = meta_properties.duplicate() + + for base_class in base_classes: + if not 'classname' in base_class: + continue + + base_str += base_class.classname + + if base_class != base_classes.back(): + base_str += ", " + + if base_str != "": + meta_props['base'] = base_str + + for prop in meta_props: + if prefix == '@SolidClass': + if prop == "size" or prop == "model": + continue + + if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM: + continue + + var value = meta_props[prop] + res += " " + prop + "(" + + if value is AABB: + res += "%s %s %s, %s %s %s" % [ + value.position.x, + value.position.y, + value.position.z, + value.size.x, + value.size.y, + value.size.z + ] + elif value is Color: + res += "%s %s %s" % [ + value.r8, + value.g8, + value.b8 + ] + elif value is String: + res += value + + res += ")" + + res += " = " + classname + + if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors + var normalized_description = description.replace("\"", "\'") + if normalized_description != "": + res += " : \"%s\" " % [normalized_description] + else: # Having no description crashes some editors + res += " : \"" + classname + "\" " + + if class_properties.size() > 0: + res += FuncGodotUtil.newline() + "[" + FuncGodotUtil.newline() + else: + res += "[" + + # Class properties + for prop in class_properties: + var value = class_properties[prop] + var prop_val = null + var prop_type := "" + var prop_description: String + if prop in class_property_descriptions: + # Optional default value for Choices can be set up as [String, int] + if value is Dictionary and class_property_descriptions[prop] is Array: + var prop_arr: Array = class_property_descriptions[prop] + if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String): + prop_description = "\"" + prop_arr[0] + "\" : " + str(prop_arr[1]) + else: + prop_description = "\"\" : 0" + printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].") + else: + prop_description = "\"" + class_property_descriptions[prop] + "\"" + else: + prop_description = "\"\"" + + match typeof(value): + TYPE_INT: + prop_type = "integer" + prop_val = str(value) + TYPE_FLOAT: + prop_type = "float" + prop_val = "\"" + str(value) + "\"" + TYPE_STRING: + prop_type = "string" + prop_val = "\"" + value + "\"" + TYPE_BOOL: + prop_type = "choices" + prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline() + prop_val += "\t\t" + str(0) + " : \"No\"" + FuncGodotUtil.newline() + prop_val += "\t\t" + str(1) + " : \"Yes\"" + FuncGodotUtil.newline() + prop_val += "\t]" + TYPE_VECTOR2, TYPE_VECTOR2I: + prop_type = "string" + prop_val = "\"%s %s\"" % [value.x, value.y] + TYPE_VECTOR3, TYPE_VECTOR3I: + prop_type = "string" + prop_val = "\"%s %s %s\"" % [value.x, value.y, value.z] + TYPE_VECTOR4, TYPE_VECTOR4I: + prop_type = "string" + prop_val = "\"%s %s %s %s\"" % [value[0], value[1], value[2], value[3]] + TYPE_COLOR: + prop_type = "color255" + prop_val = "\"%s %s %s\"" % [value.r8, value.g8, value.b8] + TYPE_DICTIONARY: + prop_type = "choices" + prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline() + for choice in value: + var choice_val = value[choice] + if typeof(choice_val) == TYPE_STRING: + if not (choice_val as String).begins_with("\""): + choice_val = "\"" + choice_val + "\"" + prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline() + prop_val += "\t]" + TYPE_ARRAY: + prop_type = "flags" + prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline() + for arr_val in value: + prop_val += "\t\t" + str(arr_val[1]) + " : \"" + str(arr_val[0]) + "\" : " + ("1" if arr_val[2] else "0") + FuncGodotUtil.newline() + prop_val += "\t]" + TYPE_NODE_PATH: + prop_type = "target_destination" + prop_val = "\"\"" + TYPE_OBJECT: + if value is Resource: + prop_val = value.resource_path + if value is Material: + if target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.JACK: + prop_type = "material" + else: + prop_type = "shader" + elif value is Texture2D: + prop_type = "decal" + elif value is AudioStream: + prop_type = "sound" + else: + prop_type = "target_source" + prop_val = "\"\"" + + if prop_val: + res += "\t" + res += prop + res += "(" + res += prop_type + res += ")" + + if not value is Array: + if not value is Dictionary or prop_description != "": + res += " : " + res += prop_description + + if value is bool or value is Dictionary or value is Array: + res += " = " + else: + res += " : " + + res += prop_val + res += FuncGodotUtil.newline() + + res += "]" + FuncGodotUtil.newline() + + return res diff --git a/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid new file mode 100644 index 00000000..88f5c99f --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid @@ -0,0 +1 @@ +uid://blhmvtghs553u diff --git a/addons/func_godot/src/fgd/func_godot_fgd_file.gd b/addons/func_godot/src/fgd/func_godot_fgd_file.gd new file mode 100644 index 00000000..feda27e5 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_file.gd @@ -0,0 +1,166 @@ +@tool +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions. Can be exported as an FGD file for use with a Quake map editor. Used in conjunction with a [FuncGodotMapSetting] resource to generate nodes in a [FuncGodotMap] node. +class_name FuncGodotFGDFile +extends Resource + +## Supported map editors enum, used in conjunction with [member target_map_editor]. +enum FuncGodotTargetMapEditors { + OTHER, + TRENCHBROOM, + JACK, + NET_RADIANT_CUSTOM, +} + +## Builds and exports the FGD file. +@export var export_file: bool: + get: + return export_file # TODO Converter40 Non existent get function + set(new_export_file): + if new_export_file != export_file: + do_export_file(target_map_editor) + +func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void: + if not Engine.is_editor_hint(): + return + + if fgd_output_folder.is_empty(): + fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String + if fgd_output_folder.is_empty(): + print("Skipping export: No game config folder") + return + + if fgd_name == "": + print("Skipping export: Empty FGD name") + + var fgd_file = fgd_output_folder + "/" + fgd_name + ".fgd" + + print("Exporting FGD to ", fgd_file) + var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE) + file_obj.store_string(build_class_text(target_editor)) + file_obj.close() + +@export_group("Map Editor") + +## Some map editors do not support the features found in others +## (ex: TrenchBroom supports the "model" key word while others require "studio", +## J.A.C.K. uses the "shader" key word while others use "material", etc...). +## If you get errors in your map editor, try changing this setting and re-exporting. +## This setting is overridden when the FGD is built via the Game Config resource. +@export var target_map_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM + +# Some map editors do not support the "model" key word and require the "studio" key word instead. +# If you get errors in your map editor, try changing this setting. +# This setting is overridden when the FGD is built via the Game Config resource. +#@export var model_key_word_supported: bool = true + +@export_group("FGD") + +## FGD output filename without the extension. +@export var fgd_name: String = "FuncGodot" + +## Array of [FuncGodotFGDFile] resources to include in FGD file output. All of the entities included with these FuncGodotFGDFile resources will be prepended to the outputted FGD file. +@export var base_fgd_files: Array[Resource] = [] + +## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap]. +@export var entity_definitions: Array[Resource] = [] + +func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String: + var res : String = "" + + for base_fgd in base_fgd_files: + if base_fgd is FuncGodotFGDFile: + res += base_fgd.build_class_text(target_editor) + else: + printerr("Base Fgd Files contains incorrect resource type! Should only be type FuncGodotFGDFile.") + + var entities = get_fgd_classes() + for ent in entities: + if not ent is FuncGodotFGDEntityClass: + continue + if ent.func_godot_internal: + continue + + var ent_text = ent.build_def_text(target_editor) + res += ent_text + if ent != entities[-1]: + res += "\n" + return res + +## This getter does a little bit of validation. Providing only an array of non-null uniquely-named entity definitions +func get_fgd_classes() -> Array: + var res : Array = [] + for cur_ent_def_ind in range(entity_definitions.size()): + var cur_ent_def = entity_definitions[cur_ent_def_ind] + if cur_ent_def == null: + continue + elif not (cur_ent_def is FuncGodotFGDEntityClass): + printerr("Bad value in entity definition set at position %s! Not an entity defintion." % cur_ent_def_ind) + continue + res.append(cur_ent_def) + return res + +func get_entity_definitions() -> Dictionary: + var res : Dictionary = {} + + for base_fgd in base_fgd_files: + var fgd_res = base_fgd.get_entity_definitions() + for key in fgd_res: + res[key] = fgd_res[key] + + for ent in get_fgd_classes(): + # Skip entities without classnames + if ent.classname.replace(" ","") == "": + printerr("Skipping " + ent.get_path() + ": Empty classname") + continue + + if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass: + var entity_def = ent.duplicate() + var meta_properties := {} + var class_properties := {} + var class_property_descriptions := {} + + for base_class in _generate_base_class_list(entity_def): + for meta_property in base_class.meta_properties: + meta_properties[meta_property] = base_class.meta_properties[meta_property] + + for class_property in base_class.class_properties: + class_properties[class_property] = base_class.class_properties[class_property] + + for class_property_desc in base_class.class_property_descriptions: + class_property_descriptions[class_property_desc] = base_class.class_property_descriptions[class_property_desc] + + for meta_property in entity_def.meta_properties: + meta_properties[meta_property] = entity_def.meta_properties[meta_property] + + for class_property in entity_def.class_properties: + class_properties[class_property] = entity_def.class_properties[class_property] + + for class_property_desc in entity_def.class_property_descriptions: + class_property_descriptions[class_property_desc] = entity_def.class_property_descriptions[class_property_desc] + + entity_def.meta_properties = meta_properties + entity_def.class_properties = class_properties + entity_def.class_property_descriptions = class_property_descriptions + + res[ent.classname] = entity_def + return res + +func _generate_base_class_list(entity_def : Resource, visited_base_classes = []) -> Array: + var base_classes : Array = [] + + visited_base_classes.append(entity_def.classname) + + # End recursive search if no more base_classes + if len(entity_def.base_classes) == 0: + return base_classes + + # Traverse up to the next level of hierarchy, if not already visited + for base_class in entity_def.base_classes: + if not base_class.classname in visited_base_classes: + base_classes.append(base_class) + base_classes += _generate_base_class_list(base_class, visited_base_classes) + else: + printerr(str("Entity '", entity_def.classname,"' contains cycle/duplicate to Entity '", base_class.classname, "'")) + + return base_classes diff --git a/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid new file mode 100644 index 00000000..9fbbdb26 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid @@ -0,0 +1 @@ +uid://cknmd0lgmorx2 diff --git a/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd b/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd new file mode 100644 index 00000000..7cd1fb1c --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd @@ -0,0 +1,166 @@ +@tool +## A special type of [FuncGodotFGDPointClass] entity that can automatically generate a special simplified GLB model file for the map editor display. +## Only supported in map editors that support GLTF or GLB. +class_name FuncGodotFGDModelPointClass +extends FuncGodotFGDPointClass + +enum TargetMapEditor { + GENERIC, + TRENCHBROOM +} + +## Determines how model interprets [member scale_expression]. +@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC +## Display model export folder relative to the model folder set by [FuncGodotLocalConfig]. +@export var models_sub_folder : String = "" +## Scale expression applied to model. See the [TrenchBroom Documentation](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) for more information. +@export var scale_expression : String = "" +## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires 'scale_expression' set to a float or [Vector3]. **WARNING:** Generated size property unlikely to align cleanly to grid! +@export var generate_size_property : bool = false +## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once. +@export var generate_gd_ignore_file : bool = false : + get: + return generate_gd_ignore_file + set(ignore): + if (ignore != generate_gd_ignore_file): + if Engine.is_editor_hint(): + var path: String = _get_game_path().path_join(_get_model_folder()) + var error: Error = DirAccess.make_dir_recursive_absolute(path) + if error != Error.OK: + printerr("Failed creating dir for GDIgnore file", error) + return + path = path.path_join('.gdignore') + if FileAccess.file_exists(path): + return + var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) + file.store_string('') + file.close() + +func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String: + _generate_model() + return super() + +func _generate_model() -> void: + if not scene_file: + return + + var gltf_state := GLTFState.new() + var path = _get_export_dir() + var node = _get_node() + if node == null: return + if not _create_gltf_file(gltf_state, path, node): + printerr("could not create gltf file") + return + node.queue_free() + if target_map_editor == TargetMapEditor.TRENCHBROOM: + const model_key: String = "model" + if scale_expression.is_empty(): + meta_properties[model_key] = '"%s"' % _get_local_path() + else: + meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [ + _get_local_path(), + scale_expression + ] + else: + meta_properties["studio"] = '"%s"' % _get_local_path() + + if generate_size_property: + meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes) + +func _get_node() -> Node3D: + var node := scene_file.instantiate() + if node is Node3D: + return node as Node3D + node.queue_free() + printerr("Scene is not of type 'Node3D'") + return null + +func _get_export_dir() -> String: + var work_dir: String = _get_game_path() + var model_dir: String = _get_model_folder() + return work_dir.path_join(model_dir).path_join('%s.glb' % classname) + +func _get_local_path() -> String: + return _get_model_folder().path_join('%s.glb' % classname) + +func _get_model_folder() -> String: + var model_dir: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.GAME_PATH_MODELS_FOLDER) as String + if not models_sub_folder.is_empty(): + model_dir = model_dir.path_join(models_sub_folder) + return model_dir + +func _get_game_path() -> String: + return FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String + +func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> bool: + var global_export_path = path + var gltf_document := GLTFDocument.new() + gltf_state.create_animations = false + + node.rotate_y(deg_to_rad(-90)) + + # With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually. + if target_map_editor != TargetMapEditor.TRENCHBROOM: + var scale_factor: Vector3 = Vector3.ONE + if scale_expression.is_empty(): + scale_factor *= FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.DEFAULT_INVERSE_SCALE) as float + else: + if scale_expression.begins_with('\''): + var scale_arr := scale_expression.split_floats(' ', false) + if scale_arr.size() == 3: + scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2]) + elif scale_expression.to_float() > 0: + scale_factor *= scale_expression.to_float() + if scale_factor.length() == 0: + scale_factor = Vector3.ONE # Don't let the node scale into oblivion! + node.scale *= scale_factor + + var error: Error = gltf_document.append_from_scene(node, gltf_state) + if error != Error.OK: + printerr("Failed appending to gltf document", error) + return false + + call_deferred("_save_to_file_system", gltf_document, gltf_state, global_export_path) + return true + +func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, path: String) -> void: + var error: Error = DirAccess.make_dir_recursive_absolute(path.get_base_dir()) + if error != Error.OK: + printerr("Failed creating dir", error) + return + + error = gltf_document.write_to_filesystem(gltf_state, path) + if error != OK: + printerr("Failed writing to file system", error) + return + print('Exported model to ', path) + +func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB: + var aabb := AABB() + for mesh in meshes: + aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb()) + + # Reorient the AABB so it matches TrenchBroom's coordinate system + var size_prop := AABB() + size_prop.position = Vector3(aabb.position.z, aabb.position.x, aabb.position.y) + size_prop.size = Vector3(aabb.size.z, aabb.size.x, aabb.size.y) + + # Scale the size bounds to our scale factor + # Scale factor will need to be set if we decide to auto-generate our bounds + var scale_factor: Vector3 = Vector3.ONE + if target_map_editor == TargetMapEditor.TRENCHBROOM: + if scale_expression.begins_with('\''): + var scale_arr := scale_expression.split_floats(' ', false) + if scale_arr.size() == 3: + scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2]) + elif scale_expression.to_float() > 0: + scale_factor *= scale_expression.to_float() + + size_prop.position *= scale_factor + size_prop.size *= scale_factor + size_prop.size += size_prop.position + # Round the size so it can stay on grid level 1 at least + for i in 3: + size_prop.position[i] = round(size_prop.position[i]) + size_prop.size[i] = round(size_prop.size[i]) + return size_prop diff --git a/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid new file mode 100644 index 00000000..4ff8c72d --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid @@ -0,0 +1 @@ +uid://dkmyelig23ub5 diff --git a/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd b/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd new file mode 100644 index 00000000..3d44fa09 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd @@ -0,0 +1,23 @@ +@tool +## FGD PointClass entity definition, used to define point entities. +## PointClass entities can use either the `node_class` or the `scene_file` property to tell [FuncGodotMap] what to generate on map build. +class_name FuncGodotFGDPointClass +extends FuncGodotFGDEntityClass + +func _init() -> void: + prefix = "@PointClass" + +@export_group ("Scene") +## An optional scene file to instantiate on map build. Overrides `node_class` and `script_class`. +@export var scene_file: PackedScene + +## An optional script file to attach to the node generated on map build. Ignored if `scene_file` is specified. +@export_group ("Scripting") +@export var script_class: Script + +@export_group("Build") +## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order. Set to `false` if you would like to define how the generated node is rotated yourself. +@export var apply_rotation_on_map_build : bool = true + +## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node. The property can be a [float], [Vector3], or [Vector2]. +@export var apply_scale_on_map_build: bool = true diff --git a/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid new file mode 100644 index 00000000..ac85f2ed --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid @@ -0,0 +1 @@ +uid://c83r7t467hm4m diff --git a/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd b/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd new file mode 100644 index 00000000..1836a3a0 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd @@ -0,0 +1,88 @@ +@tool +## FGD SolidClass entity definition, used to define brush entities. +## A [MeshInstance3D] will be generated by FuncGodotMap according to this definition's Visual Build settings. If FuncGodotFGDSolidClass [member node_class] inherits [CollisionObject3D] then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings. +class_name FuncGodotFGDSolidClass +extends FuncGodotFGDEntityClass + +enum SpawnType { + WORLDSPAWN = 0, ## Is worldspawn + MERGE_WORLDSPAWN = 1, ## Should be combined with worldspawn + ENTITY = 2, ## Is its own separate entity +} + +enum OriginType { + AVERAGED = 0, ## Use averaged brush vertices for center position. This is the old Qodot behavior. + ABSOLUTE = 1, ## Use `origin` class property in global coordinates as the center position. + RELATIVE = 2, ## Calculate center position using `origin` class property as an offset to the entity's bounding box center. + BRUSH = 3, ## Calculate center position based on the bounding box center of all brushes using the 'origin' texture specified in the [FuncGodotMapSettings]. + BOUNDS_CENTER = 4, ## Use the center of the entity's bounding box for center position. This is the default option and recommended for most entities. + BOUNDS_MINS = 5, ## Use the lowest bounding box coordinates for center position. This is standard Quake and Half-Life brush entity behavior. + BOUNDS_MAXS = 6, ## Use the highest bounding box coordinates for center position. +} + +enum CollisionShapeType { + NONE, ## No collision shape is built. Useful for decorative geometry like vines, hanging wires, grass, etc... + CONVEX, ## Will build a Convex CollisionShape3D for each brush used to make this Solid Class. Required for non-[StaticBody3D] nodes like [Area3D]. + CONCAVE ## Should have a concave collision shape +} + +## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity. +@export var spawn_type: SpawnType = SpawnType.ENTITY +## Controls how this Solid Class determines its center position. Only valid if [member spawn_type] is set to ENTITY. +@export var origin_type: OriginType = OriginType.BOUNDS_CENTER + +@export_group("Visual Build") +## Controls whether a [MeshInstance3D] is built for this Solid Class. +@export var build_visuals : bool = true +## Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping. +@export var use_in_baked_light : bool = true +## Shadow casting setting allows for further lightmapping customization. +@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON +## Automatically build [OccluderInstance3D] for this entity. +@export var build_occlusion : bool = false +## This Solid Class' [MeshInstance3D] will only be visible for [Camera3D]s whose cull mask includes any of these render layers. +@export_flags_3d_render var render_layers: int = 1 + +@export_group("Collision Build") +## Controls how collisions are built for this Solid Class. +@export var collision_shape_type: CollisionShapeType = CollisionShapeType.CONVEX +## The physics layers this Solid Class can be detected in. +@export_flags_3d_physics var collision_layer: int = 1 +## The physics layers this Solid Class scans. +@export_flags_3d_physics var collision_mask: int = 1 +## The priority used to solve colliding when penetration occurs. The higher the priority is, the lower the penetration into the Solid Class will be. This can for example be used to prevent the player from breaking through the boundaries of a level. +@export var collision_priority: float = 1.0 +## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details. +@export var collision_shape_margin: float = 0.04 + +## The following properties tell FuncGodot to add a [i]"func_godot_mesh_data"[/i] Dictionary to the metadata of the generated node upon build. +## This data is parallelized, so that each element of the array is ordered to reference the same face in the mesh. +@export_group("Mesh Metadata") +## Add a texture lookup table to the generated node's metadata on build.[br][br] +## The data is split between an [Array] of [StringName] called [i]"texture_names"[/i] containing all currently used texture materials +## and a [PackedInt32Array] called [i]"textures"[/i] where each element is an index corresponding to the [i]"texture_names"[/i] entries. +@export var add_textures_metadata: bool = false +## Add a [PackedVector3Array] called [i]"vertices"[/i] to the generated node's metadata on build.[br][br] +## This is a list of every vertex in the generated node's [MeshInstance3D]. Every 3 vertices represent a single face. +@export var add_vertex_metadata: bool = false +## Add a [PackedVector3Array] called [i]"positions"[/i] to the generated node's metadata on build.[br][br] +## This is a list of positions for each face, local to the generated node, calculated by averaging the vertices to find the face's center. +@export var add_face_position_metadata = false +## Add a [PackedVector3Array] called [i]"normals"[/i] to the generated node's metadata on build.[br][br] +## Contains a list of each face's normal. +@export var add_face_normal_metadata = false +## Add a [Dictionary] called [i]"collision_shape_to_face_range_map"[/i] in the generated node's metadata on build.[br][br] +## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of +## [Vector2i], where [i]X[/i] represents the starting index of that child's faces and [i]Y[/i] represents the +## ending index.[br][br] +## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : Vector2i(0, 15) }[/code][br][br] +## shows that this solid class has been generated with one child collision shape named +## [i]entity_1_brush_0_collision_shape[/i] which handles the first 15 faces of the parts of the mesh with collision. +@export var add_collision_shape_face_range_metadata = false + +@export_group("Scripting") +## An optional script file to attach to the node generated on map build. +@export var script_class: Script + +func _init(): + prefix = "@SolidClass" diff --git a/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid b/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid new file mode 100644 index 00000000..9d3d7ec0 --- /dev/null +++ b/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid @@ -0,0 +1 @@ +uid://msq50x6rk4po diff --git a/addons/func_godot/src/func_godot_plugin.gd b/addons/func_godot/src/func_godot_plugin.gd new file mode 100644 index 00000000..a161737a --- /dev/null +++ b/addons/func_godot/src/func_godot_plugin.gd @@ -0,0 +1,186 @@ +@tool +class_name FuncGodotPlugin +extends EditorPlugin + +var map_import_plugin : QuakeMapImportPlugin = null +var palette_import_plugin : QuakePaletteImportPlugin = null +var wad_import_plugin: QuakeWadImportPlugin = null + +var func_godot_map_control: Control = null +var func_godot_map_progress_bar: Control = null +var edited_object_ref: WeakRef = weakref(null) + +func _get_plugin_name() -> String: + return "FuncGodot" + +func _handles(object: Object) -> bool: + return object is FuncGodotMap + +func _edit(object: Object) -> void: + edited_object_ref = weakref(object) + +func _make_visible(visible: bool) -> void: + if func_godot_map_control: + func_godot_map_control.set_visible(visible) + + if func_godot_map_progress_bar: + func_godot_map_progress_bar.set_visible(visible) + +func _enter_tree() -> void: + # Import plugins + map_import_plugin = QuakeMapImportPlugin.new() + palette_import_plugin = QuakePaletteImportPlugin.new() + wad_import_plugin = QuakeWadImportPlugin.new() + + add_import_plugin(map_import_plugin) + add_import_plugin(palette_import_plugin) + add_import_plugin(wad_import_plugin) + + # FuncGodotMap button + func_godot_map_control = create_func_godot_map_control() + func_godot_map_control.set_visible(false) + add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control) + + func_godot_map_progress_bar = create_func_godot_map_progress_bar() + func_godot_map_progress_bar.set_visible(false) + add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar) + + add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null) + + ProjectSettings.set("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres") + var property_info = { + "name": "func_godot/default_map_settings", + "type": TYPE_STRING, + "hint": PROPERTY_HINT_FILE, + "hint_string": "*.tres" + } + ProjectSettings.add_property_info(property_info) + ProjectSettings.set_initial_value("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres") + +func _exit_tree() -> void: + remove_custom_type("FuncGodotMap") + remove_import_plugin(map_import_plugin) + remove_import_plugin(palette_import_plugin) + if wad_import_plugin: + remove_import_plugin(wad_import_plugin) + + map_import_plugin = null + palette_import_plugin = null + wad_import_plugin = null + + if func_godot_map_control: + remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control) + func_godot_map_control.queue_free() + func_godot_map_control = null + + if func_godot_map_progress_bar: + remove_control_from_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar) + func_godot_map_progress_bar.queue_free() + func_godot_map_progress_bar = null + +## Create the toolbar controls for [FuncGodotMap] instances in the editor +func create_func_godot_map_control() -> Control: + var separator = VSeparator.new() + + var icon = TextureRect.new() + icon.texture = preload("res://addons/func_godot/icons/icon_slipgate3d.svg") + icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER + + var build_button = Button.new() + build_button.text = "Build" + build_button.connect("pressed",Callable(self,"func_godot_map_build")) + + var unwrap_uv2_button = Button.new() + unwrap_uv2_button.text = "Unwrap UV2" + unwrap_uv2_button.connect("pressed",Callable(self,"func_godot_map_unwrap_uv2")) + + var control = HBoxContainer.new() + control.add_child(separator) + control.add_child(icon) + control.add_child(build_button) + control.add_child(unwrap_uv2_button) + + return control + +## Create a progress bar for building a [FuncGodotMap] +func create_func_godot_map_progress_bar() -> Control: + var progress_label = Label.new() + progress_label.name = "ProgressLabel" + progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + + var progress_bar := ProgressBar.new() + progress_bar.name = "ProgressBar" + progress_bar.show_percentage = false + progress_bar.min_value = 0.0 + progress_bar.max_value = 1.0 + progress_bar.custom_minimum_size.y = 30 + progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE) + progress_bar.add_child(progress_label) + progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE) + progress_label.offset_top = -9 + progress_label.offset_left = 3 + + return progress_bar + +## Create the "Build" button for [FuncGodotMap]s in the editor +func func_godot_map_build() -> void: + var edited_object : FuncGodotMap = edited_object_ref.get_ref() + if not edited_object: + return + + edited_object.should_add_children = true + edited_object.should_set_owners = true + + set_func_godot_map_control_disabled(true) + edited_object.build_progress.connect(func_godot_map_build_progress) + edited_object.build_complete.connect(func_godot_map_build_complete.bind(edited_object)) + edited_object.build_failed.connect(func_godot_map_build_complete.bind(edited_object)) + + edited_object.verify_and_build() + +## Create the "Unwrap UV2" button for [FuncGodotMap]s in the editor +func func_godot_map_unwrap_uv2() -> void: + var edited_object = edited_object_ref.get_ref() + if not edited_object: + return + + if not edited_object is FuncGodotMap: + return + + set_func_godot_map_control_disabled(true) + if not edited_object.is_connected("unwrap_uv2_complete", func_godot_map_build_complete): + edited_object.connect("unwrap_uv2_complete", func_godot_map_build_complete.bind(edited_object)) + + edited_object.unwrap_uv2() + +## Enable or disable the control for [FuncGodotMap]s in the editor +func set_func_godot_map_control_disabled(disabled: bool) -> void: + if not func_godot_map_control: + return + + for child in func_godot_map_control.get_children(): + if child is Button: + child.set_disabled(disabled) + +## Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1) +func func_godot_map_build_progress(step: String, progress: float) -> void: + var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel") + func_godot_map_progress_bar.value = progress + progress_label.text = step.capitalize() + +## Callback for when the build process for a [FuncGodotMap] is finished. +func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void: + var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel") + progress_label.text = "Build Complete" + + set_func_godot_map_control_disabled(false) + + if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")): + func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress")) + + if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")): + func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete")) + + if func_godot_map.is_connected("build_failed",Callable(self,"func_godot_map_build_complete")): + func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete")) diff --git a/addons/func_godot/src/func_godot_plugin.gd.uid b/addons/func_godot/src/func_godot_plugin.gd.uid new file mode 100644 index 00000000..836c95b6 --- /dev/null +++ b/addons/func_godot/src/func_godot_plugin.gd.uid @@ -0,0 +1 @@ +uid://c3iyymbgfvfsy diff --git a/addons/func_godot/src/import/quake_map_file.gd b/addons/func_godot/src/import/quake_map_file.gd new file mode 100644 index 00000000..9873740d --- /dev/null +++ b/addons/func_godot/src/import/quake_map_file.gd @@ -0,0 +1,6 @@ +@icon("res://addons/func_godot/icons/icon_quake_file.svg") +class_name QuakeMapFile +extends Resource + +@export var revision: int = 0 +@export_multiline var map_data: String = "" \ No newline at end of file diff --git a/addons/func_godot/src/import/quake_map_file.gd.uid b/addons/func_godot/src/import/quake_map_file.gd.uid new file mode 100644 index 00000000..4fe6cf51 --- /dev/null +++ b/addons/func_godot/src/import/quake_map_file.gd.uid @@ -0,0 +1 @@ +uid://she01epxy7i3 diff --git a/addons/func_godot/src/import/quake_map_import_plugin.gd b/addons/func_godot/src/import/quake_map_import_plugin.gd new file mode 100644 index 00000000..da6844be --- /dev/null +++ b/addons/func_godot/src/import/quake_map_import_plugin.gd @@ -0,0 +1,47 @@ +@tool +class_name QuakeMapImportPlugin +extends EditorImportPlugin + +# Quake super.map import plugin + +func _get_importer_name() -> String: + return 'func_godot.map' + +func _get_visible_name() -> String: + return 'Quake Map' + +func _get_resource_type() -> String: + return 'Resource' + +func _get_recognized_extensions() -> PackedStringArray: + return PackedStringArray(['map']) + +func _get_priority(): + return 1.0 + +func _get_save_extension() -> String: + return 'tres' + +func _get_import_options(path, preset): + return [] + +func _get_preset_count() -> int: + return 0 + +func _get_import_order(): + return 0 + +func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error: + var save_path_str = '%s.%s' % [save_path, _get_save_extension()] + + var map_resource : QuakeMapFile = null + + var existing_resource := load(save_path_str) as QuakeMapFile + if(existing_resource != null): + map_resource = existing_resource + map_resource.revision += 1 + else: + map_resource = QuakeMapFile.new() + map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text(true) + + return ResourceSaver.save(map_resource, save_path_str) \ No newline at end of file diff --git a/addons/func_godot/src/import/quake_map_import_plugin.gd.uid b/addons/func_godot/src/import/quake_map_import_plugin.gd.uid new file mode 100644 index 00000000..13cf57c1 --- /dev/null +++ b/addons/func_godot/src/import/quake_map_import_plugin.gd.uid @@ -0,0 +1 @@ +uid://cfpjdmglygjlv diff --git a/addons/func_godot/src/import/quake_palette_file.gd b/addons/func_godot/src/import/quake_palette_file.gd new file mode 100644 index 00000000..c4f2c87a --- /dev/null +++ b/addons/func_godot/src/import/quake_palette_file.gd @@ -0,0 +1,8 @@ +@icon("res://addons/func_godot/icons/icon_quake_file.svg") +class_name QuakePaletteFile +extends Resource + +@export var colors: PackedColorArray + +func _init(colors): + self.colors = colors diff --git a/addons/func_godot/src/import/quake_palette_file.gd.uid b/addons/func_godot/src/import/quake_palette_file.gd.uid new file mode 100644 index 00000000..986bb991 --- /dev/null +++ b/addons/func_godot/src/import/quake_palette_file.gd.uid @@ -0,0 +1 @@ +uid://b8dfwqf5k3175 diff --git a/addons/func_godot/src/import/quake_palette_import_plugin.gd b/addons/func_godot/src/import/quake_palette_import_plugin.gd new file mode 100644 index 00000000..01f6762e --- /dev/null +++ b/addons/func_godot/src/import/quake_palette_import_plugin.gd @@ -0,0 +1,61 @@ +@tool +class_name QuakePaletteImportPlugin +extends EditorImportPlugin + +# Quake super.map import plugin + +func _get_importer_name() -> String: + return 'func_godot.palette' + +func _get_visible_name() -> String: + return 'Quake Palette' + +func _get_resource_type() -> String: + return 'Resource' + +func _get_recognized_extensions() -> PackedStringArray: + return PackedStringArray(['lmp']) + +func _get_save_extension() -> String: + return 'tres' + +func _get_import_options(path, preset): + return [] + +func _get_preset_count() -> int: + return 0 + +func _get_priority(): + return 1.0 + +func _get_import_order(): + return 0 + +func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error: + var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()] + + var file = FileAccess.open(source_file, FileAccess.READ) + if file == null: + var err = FileAccess.get_open_error() + print(['Error opening super.lmp file: ', err]) + return err + + var colors := PackedColorArray() + + while true: + var red : int = file.get_8() + var green : int = file.get_8() + var blue : int = file.get_8() + var color := Color(red / 255.0, green / 255.0, blue / 255.0) + + colors.append(color) + + if file.eof_reached(): + break + + if colors.size() == 256: + break + + var palette_resource := QuakePaletteFile.new(colors) + + return ResourceSaver.save(palette_resource, save_path_str) \ No newline at end of file diff --git a/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid b/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid new file mode 100644 index 00000000..91fd9270 --- /dev/null +++ b/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid @@ -0,0 +1 @@ +uid://chtjgd2rk34s3 diff --git a/addons/func_godot/src/import/quake_wad_file.gd b/addons/func_godot/src/import/quake_wad_file.gd new file mode 100644 index 00000000..191c8d51 --- /dev/null +++ b/addons/func_godot/src/import/quake_wad_file.gd @@ -0,0 +1,8 @@ +@icon("res://addons/func_godot/icons/icon_quake_file.svg") +class_name QuakeWadFile +extends Resource + +@export var textures: Dictionary + +func _init(textures: Dictionary = Dictionary()): + self.textures = textures diff --git a/addons/func_godot/src/import/quake_wad_file.gd.uid b/addons/func_godot/src/import/quake_wad_file.gd.uid new file mode 100644 index 00000000..72c12019 --- /dev/null +++ b/addons/func_godot/src/import/quake_wad_file.gd.uid @@ -0,0 +1 @@ +uid://b5qusdr1oqgud diff --git a/addons/func_godot/src/import/quake_wad_import_plugin.gd b/addons/func_godot/src/import/quake_wad_import_plugin.gd new file mode 100644 index 00000000..a204e649 --- /dev/null +++ b/addons/func_godot/src/import/quake_wad_import_plugin.gd @@ -0,0 +1,209 @@ +@tool +class_name QuakeWadImportPlugin extends EditorImportPlugin + +enum WadFormat { + Quake, + HalfLife +} + +enum QuakeWadEntryType { + Palette = 0x40, + SBarPic = 0x42, + MipsTexture = 0x44, + ConsolePic = 0x45 +} + +enum HalfLifeWadEntryType { + QPic = 0x42, + MipsTexture = 0x43, + FixedFont = 0x45 +} + +const TEXTURE_NAME_LENGTH := 16 +const MAX_MIP_LEVELS := 4 + +func _get_importer_name() -> String: + return 'func_godot.wad' + +func _get_visible_name() -> String: + return 'Quake WAD' + +func _get_resource_type() -> String: + return 'Resource' + +func _get_recognized_extensions() -> PackedStringArray: + return PackedStringArray(['wad']) + +func _get_save_extension() -> String: + return 'res' + +func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool: + return true + +func _get_import_options(path, preset) -> Array[Dictionary]: + return [ + { + 'name': 'palette_file', + 'default_value': 'res://addons/func_godot/palette.lmp', + 'property_hint': PROPERTY_HINT_FILE, + 'hint_string': '*.lmp' + }, + { + 'name': 'generate_mipmaps', + 'default_value': true, + 'property_hint': PROPERTY_HINT_NONE + } + ] + +func _get_preset_count() -> int: + return 0 + +func _get_import_order() -> int: + return 0 + +func _get_priority() -> float: + return 1.0 + +func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error: + var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()] + + var file = FileAccess.open(source_file, FileAccess.READ) + if file == null: + var err = FileAccess.get_open_error() + print(['Error opening super.wad file: ', err]) + return err + + # Read WAD header + var magic : PackedByteArray = file.get_buffer(4) + var magic_string : String = magic.get_string_from_ascii() + var wad_format: int = WadFormat.Quake + + if magic_string == 'WAD3': + wad_format = WadFormat.HalfLife + elif magic_string != 'WAD2': + print('Error: Invalid WAD magic') + return ERR_INVALID_DATA + + var palette_path : String = options['palette_file'] + var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile + if wad_format == WadFormat.Quake and not palette_file: + print('Error: Invalid Quake palette file') + file.close() + return ERR_CANT_ACQUIRE_RESOURCE + + var num_entries : int = file.get_32() + var dir_offset : int = file.get_32() + + # Read entry list + file.seek(0) + file.seek(dir_offset) + + var entries : Array = [] + + for entry_idx in range(0, num_entries): + var offset : int = file.get_32() + var in_wad_size : int = file.get_32() + var size : int = file.get_32() + var type : int = file.get_8() + var compression : int = file.get_8() + var unknown : int = file.get_16() + var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH) + var name_string : String = name.get_string_from_ascii() + + if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or ( + wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)): + entries.append([ + offset, + in_wad_size, + size, + type, + compression, + name_string + ]) + + # Read mip textures + var texture_data_array: Array = [] + for entry in entries: + var offset : int = entry[0] + file.seek(offset) + + var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH) + var name_string : String = name.get_string_from_ascii() + + var width : int = file.get_32() + var height : int = file.get_32() + + var mip_offsets : Array = [] + for idx in range(0, MAX_MIP_LEVELS): + mip_offsets.append(file.get_32()) + + var num_pixels : int = width * height + var pixels : PackedByteArray = file.get_buffer(num_pixels) + + if wad_format == WadFormat.Quake: + texture_data_array.append([name_string, width, height, pixels]) + continue + # Half-Life WADs have a 256 color palette embedded in each texture + elif wad_format == WadFormat.HalfLife: + # Find the end of the mipmap data + file.seek(offset + mip_offsets[-1] + (width / 8) * (height / 8)) + file.get_16() + + var palette_colors := PackedColorArray() + for idx in 256: + var red : int = file.get_8() + var green : int = file.get_8() + var blue : int = file.get_8() + var color := Color(red / 255.0, green / 255.0, blue / 255.0) + palette_colors.append(color) + + texture_data_array.append([name_string, width, height, pixels, palette_colors]) + + # Create texture resources + var textures : Dictionary = {} + + for texture_data in texture_data_array: + var name : String = texture_data[0] + var width : int = texture_data[1] + var height : int = texture_data[2] + var pixels : PackedByteArray = texture_data[3] + + var texture_image : Image + var pixels_rgb := PackedByteArray() + + if wad_format == WadFormat.HalfLife: + var colors : PackedColorArray = texture_data[4] + for palette_color in pixels: + var rgb_color : Color = colors[palette_color] + pixels_rgb.append(rgb_color.r8) + pixels_rgb.append(rgb_color.g8) + pixels_rgb.append(rgb_color.b8) + # Color(0, 0, 255) is used for transparency in Half-Life + if rgb_color.b == 1 and rgb_color.r == 0 and rgb_color.b == 0: + pixels_rgb.append(0) + else: + pixels_rgb.append(255) + texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb) + + else: # WadFormat.Quake + for palette_color in pixels: + var rgb_color : Color = palette_file.colors[palette_color] + pixels_rgb.append(rgb_color.r8) + pixels_rgb.append(rgb_color.g8) + pixels_rgb.append(rgb_color.b8) + # Palette index 255 is used for transparency + if palette_color != 255: + pixels_rgb.append(255) + else: + pixels_rgb.append(0) + texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb) + + if options["generate_mipmaps"] == true: + texture_image.generate_mipmaps() + + var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER + textures[name] = texture + + # Save WAD resource + var wad_resource := QuakeWadFile.new(textures) + return ResourceSaver.save(wad_resource, save_path_str) diff --git a/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid b/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid new file mode 100644 index 00000000..863684ad --- /dev/null +++ b/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid @@ -0,0 +1 @@ +uid://dibj4yksw28jb diff --git a/addons/func_godot/src/map/func_godot_map.gd b/addons/func_godot/src/map/func_godot_map.gd new file mode 100644 index 00000000..4b377c30 --- /dev/null +++ b/addons/func_godot/src/map/func_godot_map.gd @@ -0,0 +1,1138 @@ +@tool +@icon("res://addons/func_godot/icons/icon_slipgate3d.svg") +## A scene generator node that parses a Quake map file using a [FuncGodotFGDFile]. Uses a [FuncGodotMapSettings] resource to define map build settings. +## To use this node, select an instance of the node in the Godot editor and select "Quick Build", "Full Build", or "Unwrap UV2" from the toolbar. Alternatively, call [method manual_build] from code. +class_name FuncGodotMap extends Node3D + +## How long to wait between child/owner batches +const YIELD_DURATION := 0.0 + +## Emitted when the build process successfully completes +signal build_complete() +## Emitted when the build process finishes a step. [code]progress[/code] is from 0.0-1.0 +signal build_progress(step, progress) +## Emitted when the build process fails +signal build_failed() + +## Emitted when UV2 unwrapping is completed +signal unwrap_uv2_complete() + +@export_category("Map") + +## Local path to Quake map file to build a scene from. +@export_file("*.map") var local_map_file: String = "" + +## Global path to Quake map file to build a scene from. Overrides [member local_map_file]. +@export_global_file("*.map") var global_map_file: String = "" + +## Map path used by code. Do it this way to support both global and local paths. +var _map_file_internal: String = "" + +## Map settings resource that defines map build scale, textures location, and more. +@export var map_settings: FuncGodotMapSettings = load(ProjectSettings.get_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")) + +@export_category("Build") +## If true, print profiling data before and after each build step. +@export var print_profiling_data: bool = false +## If true, stop the whole editor until build is complete. +@export var block_until_complete: bool = false +## How many nodes to set the owner of, or add children of, at once. Higher values may lead to quicker build times, but a less responsive editor. +@export var set_owner_batch_size: int = 1000 + +# Build context variables +var func_godot: FuncGodot = null +var profile_timestamps: Dictionary = {} +var add_child_array: Array = [] +var set_owner_array: Array = [] +var should_add_children: bool = true +var should_set_owners: bool = true +var texture_list: Array = [] +var texture_loader = null +var texture_dict: Dictionary = {} +var texture_size_dict: Dictionary = {} +var material_dict: Dictionary = {} +var entity_definitions: Dictionary = {} +var entity_dicts: Array = [] +var entity_mesh_dict: Dictionary = {} +var entity_nodes: Array = [] +var entity_mesh_instances: Dictionary = {} +var entity_occluder_instances: Dictionary = {} +var entity_collision_shapes: Array = [] + +# Utility +## Verify that FuncGodot is functioning and that [member map_file] exists. If so, build the map. If not, signal [signal build_failed] +func verify_and_build() -> void: + if verify_parameters(): + build_map() + else: + emit_signal("build_failed") + +## Build the map. +func manual_build() -> void: + should_add_children = false + should_set_owners = false + verify_and_build() + +## Return true if parameters are valid; FuncGodot should be functioning and [member map_file] should exist. +func verify_parameters() -> bool: + # Prioritize global map file path for building at runtime + _map_file_internal = global_map_file if global_map_file != "" else local_map_file + + if _map_file_internal == "": + push_error("Error: Map file not set") + return false + + if not FileAccess.file_exists(_map_file_internal): + if FileAccess.file_exists(_map_file_internal + ".import"): + _map_file_internal = _map_file_internal + ".import" + else: + push_error("Error: No such file %s" % _map_file_internal) + return false + + if not map_settings: + push_error("Error: Map settings not set") + return false + + if not func_godot: + func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new() + + if not func_godot: + push_error("Error: Failed to load func_godot.") + return false + + return true + +## Reset member variables that affect the current build +func reset_build_context() -> void: + add_child_array = [] + set_owner_array = [] + texture_list = [] + texture_loader = null + texture_dict = {} + texture_size_dict = {} + material_dict = {} + entity_definitions = {} + entity_dicts = [] + entity_mesh_dict = {} + entity_nodes = [] + entity_mesh_instances = {} + entity_occluder_instances = {} + entity_collision_shapes = [] + build_step_index = 0 + build_step_count = 0 + if func_godot: + func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new() + func_godot.map_settings = map_settings + +## Record the start time of a build step for profiling +func start_profile(item_name: String) -> void: + if print_profiling_data: + print(item_name) + profile_timestamps[item_name] = Time.get_unix_time_from_system() + +## Finish profiling for a build step; print associated timing data +func stop_profile(item_name: String) -> void: + if print_profiling_data: + if item_name in profile_timestamps: + var delta: float = Time.get_unix_time_from_system() - profile_timestamps[item_name] + print("Completed in %s sec." % snapped(delta, 0.0001)) + profile_timestamps.erase(item_name) + +## Run a build step. [code]step_name[/code] is the method corresponding to the step. +func run_build_step(step_name: String) -> Variant: + start_profile(step_name) + var result : Variant = call(step_name) + stop_profile(step_name) + return result + +## Add [code]node[/code] as a child of parent, or as a child of [code]below[/code] if non-null. Also queue for ownership assignment. +func add_child_editor(parent: Node, node: Node, below: Node = null) -> void: + if not node or not parent: + return + + var prev_parent = node.get_parent() + if prev_parent: + prev_parent.remove_child(node) + + if below: + below.add_sibling(node) + else: + parent.add_child(node) + + set_owner_array.append(node) + +## Set the owner of [code]node[/code] to the current scene. +func set_owner_editor(node: Node) -> void: + var tree : SceneTree = get_tree() + + if not tree: + return + + var edited_scene_root : Node = tree.get_edited_scene_root() + + if not edited_scene_root: + return + + node.set_owner(edited_scene_root) + +var build_step_index : int = 0 +var build_step_count : int = 0 +var build_steps : Array = [] +var post_attach_steps : Array = [] + +## Register a build step. +## [code]build_step[/code] is a string that corresponds to a method on this class, [code]arguments[/code] a list of arguments to pass to this method, and [code]target[/code] is a property on this class to save the return value of the build step in. If [code]post_attach[/code] is true, the step will be run after the scene hierarchy is completed. +func register_build_step(build_step: String, target: String = "", post_attach: bool = false) -> void: + (post_attach_steps if post_attach else build_steps).append([build_step, target]) + build_step_count += 1 + +## Run all build steps. Emits [signal build_progress] after each step. +## If [code]post_attach[/code] is true, run post-attach steps instead and signal [signal build_complete] when finished. +func run_build_steps(post_attach : bool = false) -> void: + var target_array : Array = post_attach_steps if post_attach else build_steps + + while target_array.size() > 0: + var build_step : Array = target_array.pop_front() + emit_signal("build_progress", build_step[0], float(build_step_index + 1) / float(build_step_count)) + + var scene_tree : SceneTree = get_tree() + if scene_tree and not block_until_complete: + await get_tree().create_timer(YIELD_DURATION).timeout + + var result : Variant = run_build_step(build_step[0]) + var target : String = build_step[1] + if target != "": + set(target, result) + + build_step_index += 1 + + if scene_tree and not block_until_complete: + await get_tree().create_timer(YIELD_DURATION).timeout + + if post_attach: + _build_complete() + else: + start_profile('add_children') + add_children() + +## Register all steps for the build. See [method register_build_step] and [method run_build_steps] +func register_build_steps() -> void: + register_build_step('remove_children') + register_build_step('load_map') + register_build_step('fetch_texture_list', 'texture_list') + register_build_step('init_texture_loader', 'texture_loader') + register_build_step('load_textures', 'texture_dict') + register_build_step('build_texture_size_dict', 'texture_size_dict') + register_build_step('build_materials', 'material_dict') + register_build_step('fetch_entity_definitions', 'entity_definitions') + register_build_step('set_core_entity_definitions') + register_build_step('generate_geometry') + register_build_step('fetch_entity_dicts', 'entity_dicts') + register_build_step('build_entity_nodes', 'entity_nodes') + register_build_step('resolve_trenchbroom_group_hierarchy') + register_build_step('build_entity_mesh_dict', 'entity_mesh_dict') + register_build_step('build_entity_mesh_instances', 'entity_mesh_instances') + register_build_step('build_entity_occluder_instances', 'entity_occluder_instances') + register_build_step('build_entity_collision_shape_nodes', 'entity_collision_shapes') + +## Register all post-attach steps for the build. See [method register_build_step] and [method run_build_steps] +func register_post_attach_steps() -> void: + register_build_step('build_entity_collision_shapes', "", true) + register_build_step('apply_entity_meshes', "", true) + register_build_step('apply_entity_occluders', "", true) + register_build_step('apply_properties_and_finish', "", true) + +# Actions +## Build the map +func build_map() -> void: + reset_build_context() + if map_settings == null: + printerr("Skipping build process: No map settings resource!") + emit_signal("build_complete") + return + print('Building %s' % _map_file_internal) + #if print_profiling_data: + #print('\n') + start_profile('build_map') + register_build_steps() + register_post_attach_steps() + run_build_steps() + +## Recursively unwrap UV2s for [code]node[/code] and its children, in preparation for baked lighting. +func unwrap_uv2(node: Node = null) -> void: + var target_node: Node = null + + if node: + target_node = node + else: + target_node = self + print("Unwrapping mesh UV2s") + + if target_node is MeshInstance3D: + if target_node.gi_mode == GeometryInstance3D.GI_MODE_STATIC: + var mesh: Mesh = target_node.get_mesh() + if mesh is ArrayMesh: + mesh.lightmap_unwrap(Transform3D.IDENTITY, map_settings.uv_unwrap_texel_size * map_settings.scale_factor) + + for child in target_node.get_children(): + unwrap_uv2(child) + + if not node: + print("Unwrap complete") + emit_signal("unwrap_uv2_complete") + +# Build Steps +## Recursively remove and delete all children of this node +func remove_children() -> void: + for child in get_children(): + remove_child(child) + child.queue_free() + +## Parse and load [member map_file] +func load_map() -> void: + func_godot.load_map(_map_file_internal, map_settings.use_trenchbroom_groups_hierarchy) + +## Get textures found in [member map_file] +func fetch_texture_list() -> Array: + return func_godot.get_texture_list() as Array + +## Initialize texture loader, allowing textures in [member base_texture_dir] and [member texture_wads] to be turned into materials +func init_texture_loader() -> FuncGodotTextureLoader: + return FuncGodotTextureLoader.new(map_settings) + +## Build a dictionary from Map File texture names to their corresponding Texture2D resources in Godot +func load_textures() -> Dictionary: + return texture_loader.load_textures(texture_list) as Dictionary + +## Build a dictionary from Map File texture names to Godot materials +func build_materials() -> Dictionary: + return texture_loader.create_materials(texture_list) + +## Collect entity definitions from [member entity_fgd], as a dictionary from Map File classnames to entity definitions +func fetch_entity_definitions() -> Dictionary: + return map_settings.entity_fgd.get_entity_definitions() + +## Hand the FuncGodot core the entity definitions +func set_core_entity_definitions() -> void: + var core_ent_defs: Dictionary = {} + for classname in entity_definitions: + core_ent_defs[classname] = {} + var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] + if entity_definition is FuncGodotFGDSolidClass: + core_ent_defs[classname]['spawn_type'] = entity_definition.spawn_type + core_ent_defs[classname]['origin_type'] = entity_definition.origin_type + + const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags + var flags := MFlags.NONE + if entity_definition.add_textures_metadata: flags |= MFlags.TEXTURES + if entity_definition.add_vertex_metadata: flags |= MFlags.VERTEX + if entity_definition.add_face_normal_metadata: flags |= MFlags.FACE_NORMAL + if entity_definition.add_face_position_metadata: flags |= MFlags.FACE_POSITION + if entity_definition.add_collision_shape_face_range_metadata: flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP + core_ent_defs[classname]['metadata_inclusion_flags'] = flags + func_godot.set_entity_definitions(core_ent_defs) + +## Generate geometry from map file +func generate_geometry() -> void: + func_godot.generate_geometry(texture_size_dict); + +## Get a list of dictionaries representing each entity from the FuncGodot core +func fetch_entity_dicts() -> Array: + return func_godot.get_entity_dicts() + +## Build a dictionary from Map File textures to the sizes of their corresponding Godot textures +func build_texture_size_dict() -> Dictionary: + var texture_size_dict: Dictionary = {} + + for tex_key in texture_dict: + var texture: Texture2D = texture_dict[tex_key] as Texture2D + if texture: + texture_size_dict[tex_key] = texture.get_size() + else: + texture_size_dict[tex_key] = Vector2.ONE + + return texture_size_dict + +## Build nodes from the entities in [member entity_dicts] +func build_entity_nodes() -> Array: + var entity_nodes : Array = [] + entity_nodes.resize(entity_dicts.size()) + + # TrenchBroom: Prevent generation of omitted layers + var omitted_entities : Array[int] = [] + var omitted_groups: Array[String] = [] + + if map_settings.use_trenchbroom_groups_hierarchy: + # Omit layers + for entity_idx in range(0, entity_dicts.size()): + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] as Dictionary + + if '_tb_type' in properties and properties['_tb_type'] == '_tb_layer': + if '_tb_layer_omit_from_export' in properties and properties['_tb_layer_omit_from_export'] == "1": + omitted_entities.append(entity_idx) + omitted_groups.append("layer_" + str(properties.get('_tb_id', "-1"))) + + # Omit groups and top-level entities + for entity_idx in range(0, entity_dicts.size()): + if omitted_entities.find(entity_idx) != -1: + continue + + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] as Dictionary + + if '_tb_layer' in properties: + if omitted_groups.find("layer_" + str(properties['_tb_layer'])) != -1: + omitted_entities.append(entity_idx) + if '_tb_id' in properties and properties['_tb_type'] == '_tb_group': + omitted_groups.append("group_" + str(properties.get('_tb_id', "-1"))) + + for entity_idx in range(0, entity_dicts.size()): + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] as Dictionary + + if map_settings.use_trenchbroom_groups_hierarchy: + if omitted_entities.find(entity_idx) != -1: + entity_nodes[entity_idx] = null + continue + if '_tb_group' in properties and omitted_groups.find("group_" + str(properties['_tb_group'])) != -1: + entity_nodes[entity_idx] = null + continue + + var node: Node = null + var node_name: String = "entity_%s" % entity_idx + + var should_add_child: bool = should_add_children + + if 'classname' in properties: + var classname: String = properties['classname'] + node_name += "_" + classname + if classname in entity_definitions: + var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass + + var name_prop: String + if entity_definition.name_property in properties: + name_prop = str(properties[entity_definition.name_property]) + elif map_settings.entity_name_property in properties: + name_prop = str(properties[map_settings.entity_name_property]) + if not name_prop.is_empty(): + node_name = "entity_" + name_prop + + if entity_definition is FuncGodotFGDSolidClass: + if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN: + entity_nodes[entity_idx] = null + continue + if entity_definition.node_class != "": + node = ClassDB.instantiate(entity_definition.node_class) + elif entity_definition is FuncGodotFGDPointClass: + if entity_definition.scene_file: + var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED + if Engine.is_editor_hint(): + flag = PackedScene.GEN_EDIT_STATE_INSTANCE + node = entity_definition.scene_file.instantiate(flag) + elif entity_definition.node_class != "": + node = ClassDB.instantiate(entity_definition.node_class) + if 'rotation_degrees' in node and entity_definition.apply_rotation_on_map_build: + var angles := Vector3.ZERO + if 'angles' in properties or 'mangle' in properties: + var key := 'angles' if 'angles' in properties else 'mangle' + var angles_raw = properties[key] + if not angles_raw is Vector3: + angles_raw = angles_raw.split_floats(' ') + if angles_raw.size() > 2: + angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2]) + if key == 'mangle': + if entity_definition.classname.begins_with('light'): + angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2]) + elif entity_definition.classname == 'info_intermission': + angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2]) + else: + push_error("Invalid vector format for \'" + key + "\' in entity \'" + classname + "\'") + elif 'angle' in properties: + var angle = properties['angle'] + if not angle is float: + angle = float(angle) + angles.y += angle + angles.y += 180 + node.rotation_degrees = angles + + if 'scale' in node and entity_definition.apply_scale_on_map_build: + if 'scale' in properties: + var scale_prop: Variant = properties['scale'] + if typeof(scale_prop) == TYPE_STRING: + var scale_arr: PackedStringArray = (scale_prop as String).split(" ") + match scale_arr.size(): + 1: scale_prop = scale_arr[0].to_float() + 3: scale_prop = Vector3(scale_arr[1].to_float(), scale_arr[2].to_float(), scale_arr[0].to_float()) + 2: scale_prop = Vector2(scale_arr[0].to_float(), scale_arr[0].to_float()) + if typeof(scale_prop) == TYPE_FLOAT or typeof(scale_prop) == TYPE_INT: + node.scale *= scale_prop as float + elif node.scale is Vector3: + if typeof(scale_prop) == TYPE_VECTOR3 or typeof(scale_prop) == TYPE_VECTOR3I: + node.scale *= scale_prop as Vector3 + elif node.scale is Vector2: + if typeof(scale_prop) == TYPE_VECTOR2 or typeof(scale_prop) == TYPE_VECTOR2I: + node.scale *= scale_prop as Vector2 + else: + node = Node3D.new() + if entity_definition.script_class: + node.set_script(entity_definition.script_class) + if not node: + node = Node3D.new() + + node.name = node_name + + if 'origin' in properties and entity_dict.brush_count < 1: + var origin_vec: Vector3 = Vector3.ZERO + var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ') + if origin_comps.size() > 2: + origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0]) + else: + push_error("Invalid vector format for \'origin\' in " + node.name) + if 'position' in node: + if node.position is Vector3: + node.position = origin_vec * map_settings.scale_factor + elif node.position is Vector2: + node.position = Vector2(origin_vec.z, -origin_vec.y) + else: + if entity_idx != 0 and 'position' in node: + if node.position is Vector3: + node.position = entity_dict['center'] * map_settings.scale_factor + + entity_nodes[entity_idx] = node + + if should_add_child: + queue_add_child(self, node) + + return entity_nodes + +## Build [CollisionShape3D] nodes for brush entities +func build_entity_collision_shape_nodes() -> Array: + var entity_collision_shapes_arr: Array = [] + + for entity_idx in range(0, entity_nodes.size()): + var entity_collision_shapes: Array = [] + + var entity_dict: Dictionary = entity_dicts[entity_idx] + var properties: Dictionary = entity_dict['properties'] + + var node: Node = entity_nodes[entity_idx] as Node + var concave: bool = false + + if 'classname' in properties: + var classname: String = properties['classname'] + if classname in entity_definitions: + var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass + if entity_definition: + if entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.NONE: + entity_collision_shapes_arr.append(null) + continue + elif entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE: + concave = true + + if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN: + # TODO: Find the worldspawn object instead of assuming index 0 + node = entity_nodes[0] as Node + + if node and node is CollisionObject3D: + (node as CollisionObject3D).collision_layer = entity_definition.collision_layer + (node as CollisionObject3D).collision_mask = entity_definition.collision_mask + (node as CollisionObject3D).collision_priority = entity_definition.collision_priority + + # don't create collision shapes that wont be attached to a CollisionObject3D as they are a waste + if not node or (not node is CollisionObject3D): + entity_collision_shapes_arr.append(null) + continue + + if concave: + var collision_shape: CollisionShape3D = CollisionShape3D.new() + collision_shape.name = "entity_%s_collision_shape" % entity_idx + entity_collision_shapes.append(collision_shape) + queue_add_child(node, collision_shape) + else: + for brush_idx in entity_dict['brush_indices']: + var collision_shape: CollisionShape3D = CollisionShape3D.new() + collision_shape.name = "entity_%s_brush_%s_collision_shape" % [entity_idx, brush_idx] + entity_collision_shapes.append(collision_shape) + queue_add_child(node, collision_shape) + entity_collision_shapes_arr.append(entity_collision_shapes) + + return entity_collision_shapes_arr + +## Build the concrete [Shape3D] resources for each brush +func build_entity_collision_shapes() -> void: + for entity_idx in range(0, entity_dicts.size()): + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] + var entity_position: Vector3 = Vector3.ZERO + if entity_nodes[entity_idx] != null and entity_nodes[entity_idx].get("position"): + if entity_nodes[entity_idx].position is Vector3: + entity_position = entity_nodes[entity_idx].position + + if entity_collision_shapes.size() < entity_idx: + continue + if entity_collision_shapes[entity_idx] == null: + continue + + var entity_collision_shape: Array = entity_collision_shapes[entity_idx] + var concave: bool = false + var shape_margin: float = 0.04 + var entity_definition: FuncGodotFGDSolidClass + + if 'classname' in properties: + var classname: String = properties['classname'] + if classname in entity_definitions: + entity_definition = entity_definitions[classname] as FuncGodotFGDSolidClass + if entity_definition: + match(entity_definition.collision_shape_type): + FuncGodotFGDSolidClass.CollisionShapeType.NONE: + continue + FuncGodotFGDSolidClass.CollisionShapeType.CONVEX: + concave = false + FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE: + concave = true + shape_margin = entity_definition.collision_shape_margin + + if entity_collision_shapes[entity_idx] == null: + continue + + if concave: + func_godot.gather_entity_concave_collision_surfaces(entity_idx) + else: + func_godot.gather_entity_convex_collision_surfaces(entity_idx) + var entity_surfaces: Array = func_godot.fetch_surfaces(func_godot.surface_gatherer) + var metadata: Dictionary = func_godot.surface_gatherer.out_metadata + var collision_shape_to_face_range_map: Dictionary = {} + var face_shape_indices: Array[Vector2i] = metadata["shape_index_ranges"] + + var entity_verts: PackedVector3Array = PackedVector3Array() + + for surface_idx in range(0, entity_surfaces.size()): + if entity_surfaces[surface_idx] == null: + continue + + var surface_verts: Array = entity_surfaces[surface_idx] + + if concave: + var vertices: PackedVector3Array = surface_verts[Mesh.ARRAY_VERTEX] as PackedVector3Array + var indices: PackedInt32Array = surface_verts[Mesh.ARRAY_INDEX] as PackedInt32Array + for vert_idx in indices: + entity_verts.append(vertices[vert_idx]) + else: + var shape_points = PackedVector3Array() + for vertex in surface_verts[Mesh.ARRAY_VERTEX]: + if not vertex in shape_points: + shape_points.append(vertex) + + var shape: ConvexPolygonShape3D = ConvexPolygonShape3D.new() + shape.set_points(shape_points) + shape.margin = shape_margin + + var collision_shape: CollisionShape3D = entity_collision_shape[surface_idx] + collision_shape.set_shape(shape) + + # For face shape range metadata, we need to add info about child node names + if entity_definition and entity_definition.add_collision_shape_face_range_metadata: + collision_shape_to_face_range_map[collision_shape.name] = face_shape_indices[surface_idx] + + if concave: + if entity_verts.size() == 0: + continue + + var shape: ConcavePolygonShape3D = ConcavePolygonShape3D.new() + shape.set_faces(entity_verts) + shape.margin = shape_margin + + var collision_shape: CollisionShape3D = entity_collision_shapes[entity_idx][0] + collision_shape.set_shape(shape) + + if entity_definition and entity_definition.add_collision_shape_face_range_metadata: + collision_shape_to_face_range_map[collision_shape.name] = Vector2i(0, entity_verts.size() / 3) + + if entity_definition: + if not entity_definition.add_face_normal_metadata: + metadata.erase("normals") + if not entity_definition.add_face_position_metadata: + metadata.erase("positions") + if not entity_definition.add_textures_metadata: + metadata.erase("textures") + metadata.erase("texture_names") + if not entity_definition.add_vertex_metadata: + metadata.erase("vertices") + + metadata.erase("shape_index_ranges") # cleanup intermediate / buffer + if entity_definition.add_collision_shape_face_range_metadata: + metadata["collision_shape_to_face_range_map"] = collision_shape_to_face_range_map + + if not metadata.is_empty(): + entity_nodes[entity_idx].set_meta("func_godot_mesh_data", metadata) + +## Build Dictionary from entity indices to [ArrayMesh] instances +func build_entity_mesh_dict() -> Dictionary: + var meshes: Dictionary = {} + + var texture_surf_map: Dictionary + var texture_to_metadata_map: Dictionary + for texture in texture_dict: + texture_surf_map[texture] = Array() + texture_to_metadata_map[texture] = {} + + var gather_task = func(i): + var texture: String = texture_dict.keys()[i] + var fetch_result = func_godot.gather_texture_surfaces(texture) + texture_surf_map[texture] = fetch_result["surfaces"] + texture_to_metadata_map[texture] = fetch_result["metadata"] + + var task_id: int = WorkerThreadPool.add_group_task(gather_task, texture_dict.keys().size(), 4, true) + WorkerThreadPool.wait_for_group_task_completion(task_id) + + for texture in texture_dict: + var texture_surfaces: Array = texture_surf_map[texture] as Array + + for entity_idx in range(0, texture_surfaces.size()): + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] + + var entity_surface = texture_surfaces[entity_idx] + var entity_definition: FuncGodotFGDSolidClass + + if 'classname' in properties: + var classname: String = properties['classname'] + if classname in entity_definitions: + entity_definition = entity_definitions[classname] as FuncGodotFGDSolidClass + if entity_definition: + if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN: + entity_surface = null + + if not entity_definition.build_visuals and not entity_definition.build_occlusion: + entity_surface = null + + if entity_surface == null: + continue + + if not entity_idx in meshes: + meshes[entity_idx] = ArrayMesh.new() + + var mesh: ArrayMesh = meshes[entity_idx] + mesh.add_surface_from_arrays(ArrayMesh.PRIMITIVE_TRIANGLES, entity_surface) + mesh.surface_set_name(mesh.get_surface_count() - 1, texture) + mesh.surface_set_material(mesh.get_surface_count() - 1, material_dict[texture]) + + # Build metadata only if the node is set to not build collision. Otherwise we are already building it in build_entity_collision_shapes. + if entity_definition and entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.NONE: + if not mesh.has_meta("func_godot_mesh_data"): + mesh.set_meta("func_godot_mesh_data", Dictionary()) + var this_textures_metadata: Dictionary = texture_to_metadata_map[texture] + var entity_metadata: Dictionary = mesh.get_meta("func_godot_mesh_data") + var entity_index_ranges: Array[Vector2i] = this_textures_metadata["entity_index_ranges"] + var range: Vector2i = entity_index_ranges[entity_idx] + + if entity_definition.add_vertex_metadata: + var vertices: PackedVector3Array = entity_metadata.get("vertices", PackedVector3Array()) + vertices.append_array((this_textures_metadata["vertices"] as PackedVector3Array).slice(range.x * 3, range.y * 3)) + entity_metadata["vertices"] = vertices + + if entity_definition.add_face_normal_metadata: + var normals: PackedVector3Array = entity_metadata.get("normals", PackedVector3Array()) + normals.append_array((this_textures_metadata["normals"] as PackedVector3Array).slice(range.x, range.y)) + entity_metadata["normals"] = normals + + if entity_definition.add_face_position_metadata: + var positions: PackedVector3Array = entity_metadata.get("positions", PackedVector3Array()) + positions.append_array((this_textures_metadata["positions"] as PackedVector3Array).slice(range.x, range.y)) + entity_metadata["positions"] = positions + + if entity_definition.add_textures_metadata: + # different (if null: add empty) logic for texture_names due to not being able make a static typed + # Array[StringName] inline in the get() function + if not entity_metadata.has("texture_names"): + var new: Array[StringName] = [] + entity_metadata["texture_names"] = new + var texture_names: Array[StringName] = entity_metadata["texture_names"] + var textures: PackedInt32Array = entity_metadata.get("textures", PackedInt32Array()) + var texture_block: PackedInt32Array = [] + texture_block.resize(range.y - range.x) + texture_block.fill(texture_names.size()) + texture_names.append(StringName(texture)) + textures.append_array(texture_block) + entity_metadata["textures"] = textures + + return meshes + +## Build [MeshInstance3D]s from brush entities and add them to the add child queue +func build_entity_mesh_instances() -> Dictionary: + var entity_mesh_instances: Dictionary = {} + for entity_idx in entity_mesh_dict: + var use_in_baked_light: bool = false + var shadow_casting_setting: GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED + var render_layers: int = 1 + + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] + var classname: String = properties['classname'] + if classname in entity_definitions: + var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass + if entity_definition: + if not entity_definition.build_visuals: + continue + + if entity_definition.use_in_baked_light: + use_in_baked_light = true + elif '_shadow' in properties: + if properties['_shadow'] == "1": + use_in_baked_light = true + shadow_casting_setting = entity_definition.shadow_casting_setting + render_layers = entity_definition.render_layers + + if not entity_mesh_dict[entity_idx]: + continue + + var mesh_instance: MeshInstance3D = MeshInstance3D.new() + mesh_instance.name = 'entity_%s_mesh_instance' % entity_idx + mesh_instance.gi_mode = MeshInstance3D.GI_MODE_STATIC if use_in_baked_light else GeometryInstance3D.GI_MODE_DISABLED + mesh_instance.cast_shadow = shadow_casting_setting + mesh_instance.layers = render_layers + + queue_add_child(entity_nodes[entity_idx], mesh_instance) + + entity_mesh_instances[entity_idx] = mesh_instance + + return entity_mesh_instances + +func build_entity_occluder_instances() -> Dictionary: + var entity_occluder_instances: Dictionary = {} + for entity_idx in entity_mesh_dict: + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] + var classname: String = properties['classname'] + if classname in entity_definitions: + var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass + if entity_definition: + if entity_definition.build_occlusion: + if not entity_mesh_dict[entity_idx]: + continue + + var occluder_instance: OccluderInstance3D = OccluderInstance3D.new() + occluder_instance.name = 'entity_%s_occluder_instance' % entity_idx + + queue_add_child(entity_nodes[entity_idx], occluder_instance) + entity_occluder_instances[entity_idx] = occluder_instance + + return entity_occluder_instances + +## Assign [ArrayMesh]es to their [MeshInstance3D] counterparts +func apply_entity_meshes() -> void: + for entity_idx in entity_mesh_instances: + var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh + var mesh_instance: MeshInstance3D = entity_mesh_instances[entity_idx] as MeshInstance3D + if not mesh or not mesh_instance: + if mesh.has_meta("func_godot_mesh_data"): + mesh.remove_meta("func_godot_mesh_data") + continue + + mesh_instance.set_mesh(mesh) + queue_add_child(entity_nodes[entity_idx], mesh_instance) + if mesh.has_meta("func_godot_mesh_data"): + entity_nodes[entity_idx].set_meta("func_godot_mesh_data", mesh.get_meta("func_godot_mesh_data")) + mesh.remove_meta("func_godot_mesh_data") + +func apply_entity_occluders() -> void: + for entity_idx in entity_mesh_dict: + var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh + var occluder_instance: OccluderInstance3D + + if entity_idx in entity_occluder_instances: + occluder_instance = entity_occluder_instances[entity_idx] + + if not mesh or not occluder_instance: + continue + + var verts: PackedVector3Array + var indices: PackedInt32Array + var index: int = 0 + for surf_idx in range(mesh.get_surface_count()): + var vert_count: int = verts.size() + var surf_array: Array = mesh.surface_get_arrays(surf_idx) + verts.append_array(surf_array[Mesh.ARRAY_VERTEX]) + indices.resize(indices.size() + surf_array[Mesh.ARRAY_INDEX].size()) + for new_index in surf_array[Mesh.ARRAY_INDEX]: + indices[index] = (new_index + vert_count) + index += 1 + + var occluder: ArrayOccluder3D = ArrayOccluder3D.new() + occluder.set_arrays(verts, indices) + + occluder_instance.occluder = occluder + +## Resolve entity group hierarchy, turning Trenchbroom groups into nodes and queueing their contents to be added to said nodes as children +func resolve_trenchbroom_group_hierarchy() -> void: + if not map_settings.use_trenchbroom_groups_hierarchy: + return + + var parent_entities: Dictionary = {} + var child_entities: Dictionary = {} + + # Gather all entities which are children in some group or parents in some group + for node_idx in range(0, entity_nodes.size()): + var node: Node = entity_nodes[node_idx] + var properties: Dictionary = entity_dicts[node_idx]['properties'] + + if not properties: + continue + + if not ('_tb_id' in properties or '_tb_group' in properties or '_tb_layer' in properties): + continue + + # identify children + if '_tb_group' in properties or '_tb_layer' in properties: + child_entities[node_idx] = node + + # identify parents + if '_tb_id' in properties: + node.set_meta("_tb_type", properties['_tb_type']) + if properties['_tb_type'] == "_tb_group": + node.name = "group_" + str(properties['_tb_id']) + elif properties['_tb_type'] == "_tb_layer": + node.name = "layer_" + str(properties['_tb_layer_sort_index']) + if properties['_tb_name'] != "Unnamed": + node.name = node.name + "_" + properties['_tb_name'] + parent_entities[node_idx] = node + + var child_to_parent_map: Dictionary = {} + + #For each child,... + for node_idx in child_entities: + var node: Node = child_entities[node_idx] + var properties: Dictionary = entity_dicts[node_idx]['properties'] + var tb_group: Variant = null + if '_tb_group' in properties: + tb_group = properties['_tb_group'] + elif '_tb_layer' in properties: + tb_group = properties['_tb_layer'] + if tb_group == null: + continue + + var parent: Node = null + var parent_properties: Dictionary = {} + var parent_entity = null + var parent_idx = null + + # ...identify its direct parent out of the parent_entities array + for possible_parent in parent_entities: + parent_entity = parent_entities[possible_parent] + parent_properties = entity_dicts[possible_parent]['properties'] + if parent_properties['_tb_id'] == tb_group: + parent = parent_entity + parent_idx = possible_parent + break + # If there's a match, pass it on to the child-parent relationship map + if parent: + child_to_parent_map[node_idx] = parent_idx + + for child_idx in child_to_parent_map: + var child = entity_nodes[child_idx] + var parent_idx = child_to_parent_map[child_idx] + var parent = entity_nodes[parent_idx] + queue_add_child(parent, child, null, true) + +## Add a child and its new parent to the add child queue. If [code]below[/code] is a node, add it as a child to that instead. If [code]relative[/code] is true, set the location of node relative to parent. +func queue_add_child(parent, node, below = null, relative = false) -> void: + add_child_array.append({"parent": parent, "node": node, "below": below, "relative": relative}) + +## Assign children to parents based on the contents of the add child queue (see [method queue_add_child]) +func add_children() -> void: + while true: + for i in range(0, set_owner_batch_size): + if add_child_array.size() > 0: + var data: Dictionary = add_child_array.pop_front() + if data: + add_child_editor(data['parent'], data['node'], data['below']) + if data['relative']: + if (data['node'] is Node3D and data['parent'] is Node3D) or (data['node'] is Node2D and data['parent'] is Node2D): + data['node'].position -= data['parent'].position + continue + add_children_complete() + return + + var scene_tree: SceneTree = get_tree() + if scene_tree and not block_until_complete: + await get_tree().create_timer(YIELD_DURATION).timeout + +## Set owners and start post-attach build steps +func add_children_complete() -> void: + stop_profile('add_children') + + if should_set_owners: + start_profile('set_owners') + set_owners() + else: + run_build_steps(true) + +## Set owner of nodes generated by FuncGodot to scene root based on [member set_owner_array] +func set_owners() -> void: + while true: + for i in range(0, set_owner_batch_size): + var node: Node = set_owner_array.pop_front() + if node: + set_owner_editor(node) + else: + set_owners_complete() + return + + var scene_tree: SceneTree = get_tree() + if scene_tree and not block_until_complete: + await get_tree().create_timer(YIELD_DURATION).timeout + +## Finish profiling for set_owners and start post-attach build steps +func set_owners_complete() -> void: + stop_profile('set_owners') + run_build_steps(true) + +## Apply Map File properties to [Node3D] instances, transferring Map File dictionaries to [Node3D.func_godot_properties] +## and then calling the appropriate callbacks. +func apply_properties_and_finish() -> void: + for entity_idx in range(0, entity_nodes.size()): + var entity_node: Node = entity_nodes[entity_idx] as Node + if not entity_node: + continue + + var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary + var properties: Dictionary = entity_dict['properties'] as Dictionary + + if 'classname' in properties: + var classname: String = properties['classname'] + if classname in entity_definitions: + var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass + + for property in properties: + var prop_string = properties[property] + if property in entity_definition.class_properties: + var prop_default: Variant = entity_definition.class_properties[property] + + match typeof(prop_default): + TYPE_INT: + properties[property] = prop_string.to_int() + TYPE_FLOAT: + properties[property] = prop_string.to_float() + TYPE_BOOL: + properties[property] = bool(prop_string.to_int()) + TYPE_VECTOR3: + var prop_comps: PackedFloat64Array = prop_string.split_floats(" ") + if prop_comps.size() > 2: + properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2]) + else: + push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_default + TYPE_VECTOR3I: + var prop_vec: Vector3i = prop_default + var prop_comps: PackedStringArray = prop_string.split(" ") + if prop_comps.size() > 2: + for i in 3: + prop_vec[i] = prop_comps[i].to_int() + else: + push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_vec + TYPE_COLOR: + var prop_color: Color = prop_default + var prop_comps: PackedStringArray = prop_string.split(" ") + if prop_comps.size() > 2: + prop_color.r8 = prop_comps[0].to_int() + prop_color.g8 = prop_comps[1].to_int() + prop_color.b8 = prop_comps[2].to_int() + prop_color.a = 1.0 + else: + push_error("Invalid Color format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_color + TYPE_DICTIONARY: + var prop_desc = entity_definition.class_property_descriptions[property] + if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int: + properties[property] = prop_string.to_int() + TYPE_ARRAY: + properties[property] = prop_string.to_int() + TYPE_VECTOR2: + var prop_comps: PackedFloat64Array = prop_string.split_floats(" ") + if prop_comps.size() > 1: + properties[property] = Vector2(prop_comps[0], prop_comps[1]) + else: + push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_default + TYPE_VECTOR2I: + var prop_vec: Vector2i = prop_default + var prop_comps: PackedStringArray = prop_string.split(" ") + if prop_comps.size() > 1: + for i in 2: + prop_vec[i] = prop_comps[i].to_int() + else: + push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_vec + TYPE_VECTOR4: + var prop_comps: PackedFloat64Array = prop_string.split_floats(" ") + if prop_comps.size() > 3: + properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3]) + else: + push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_default + TYPE_VECTOR4I: + var prop_vec: Vector4i = prop_default + var prop_comps: PackedStringArray = prop_string.split(" ") + if prop_comps.size() > 3: + for i in 4: + prop_vec[i] = prop_comps[i].to_int() + else: + push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string) + properties[property] = prop_vec + TYPE_NODE_PATH: + properties[property] = prop_string + TYPE_OBJECT: + properties[property] = prop_string + + # Assign properties not defined with defaults from the entity definition + for property in entity_definitions[classname].class_properties: + if not property in properties: + var prop_default: Variant = entity_definition.class_properties[property] + # Flags + if prop_default is Array: + var prop_flags_sum := 0 + for prop_flag in prop_default: + if prop_flag is Array and prop_flag.size() > 2: + if prop_flag[2] and prop_flag[1] is int: + prop_flags_sum += prop_flag[1] + properties[property] = prop_flags_sum + # Choices + elif prop_default is Dictionary: + var prop_desc = entity_definition.class_property_descriptions[property] + if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String): + properties[property] = prop_desc[1] + else: + properties[property] = 0 + elif prop_default is Resource: + properties[property] = prop_default.resource_path + elif prop_default is NodePath or prop_default is Object or prop_default == null: + properties[property] = "" + # Everything else + else: + properties[property] = prop_default + + if entity_definition.auto_apply_to_matching_node_properties: + for property in properties: + if property in entity_node: + if typeof(entity_node.get(property)) == typeof(properties[property]): + entity_node.set(property, properties[property]) + else: + push_error("Entity %s property \'%s\' type mismatch with matching generated node property." % [entity_node.name, property]) + + if 'func_godot_properties' in entity_node: + entity_node.func_godot_properties = properties + + if entity_node.has_method("_func_godot_apply_properties"): + entity_node.call("_func_godot_apply_properties", properties) + + if entity_node.has_method("_func_godot_build_complete"): + entity_node.call_deferred("_func_godot_build_complete") + +# Cleanup after build is finished (internal) +func _build_complete(): + reset_build_context() + stop_profile('build_map') + print('Build complete\n') + emit_signal("build_complete") diff --git a/addons/func_godot/src/map/func_godot_map.gd.uid b/addons/func_godot/src/map/func_godot_map.gd.uid new file mode 100644 index 00000000..16635b89 --- /dev/null +++ b/addons/func_godot/src/map/func_godot_map.gd.uid @@ -0,0 +1 @@ +uid://kno58homctew diff --git a/addons/func_godot/src/map/func_godot_map_settings.gd b/addons/func_godot/src/map/func_godot_map_settings.gd new file mode 100644 index 00000000..fe974421 --- /dev/null +++ b/addons/func_godot/src/map/func_godot_map_settings.gd @@ -0,0 +1,85 @@ +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +@tool +## Reusable map settings configuration for [FuncGodotMap] nodes. +class_name FuncGodotMapSettings +extends Resource + +## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number when building. This does not affect entity properties unless scripted to do so. +var scale_factor: float = 0.03125 +@export var inverse_scale_factor: float = 32.0 : + set(value): + inverse_scale_factor = value + scale_factor = 1.0 / value + +## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes. +@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") + +## Default class property to use in naming generated nodes. This setting is overridden by `name_property` in [FuncGodotFGDEntityClass]. +## Naming occurs before adding to the [SceneTree] and applying properties. +## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior. +@export var entity_name_property: String = "" + +@export_category("Textures") + +## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces. +@export_dir var base_texture_dir: String = "res://textures" + +## File extensions to search for texture data. +@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"] + +## Optional path for the clip texture, relative to [member base_texture_dir]. Brush faces textured with the clip texture will have those faces removed from the generated [MeshInstance3D] but not the generated [CollisionShape3D]. +@export var clip_texture: String = "special/clip" + +## Optional path for the skip texture, relative to [member base_texture_dir]. Brush faces textured with the skip texture will have those faces removed from the generated [MeshInstance3D]. If the [FuncGodotFGDSolidClass] `collision_shape_type` is set to concave then it will also remove collision from those faces in the generated [CollisionShape3D]. +@export var skip_texture: String = "special/skip" + +## Optional path for the origin texture, relative to [member base_texture_dir]. Brush faces textured with the origin texture will have those faces removed from the generated [MeshInstance3D]. The bounds of these faces will be used to calculate the origin point of the entity. +@export var origin_texture: String = "special/origin" + +## Optional [QuakeWADFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs. +@export var texture_wads: Array[Resource] = [] + +@export_category("Materials") + +## File extension to search for [Material] definitions +@export var material_file_extension: String = "tres" + +## [Material] used as template when generating missing materials. +@export var default_material: Material = preload("res://addons/func_godot/textures/default_material.tres") + +## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial]. +@export var default_material_albedo_uniform: String = "" + +## Automatic PBR material generation albedo map pattern. +@export var albedo_map_pattern: String = "%s_albedo.%s" +## Automatic PBR material generation normal map pattern. +@export var normal_map_pattern: String = "%s_normal.%s" +## Automatic PBR material generation metallic map pattern +@export var metallic_map_pattern: String = "%s_metallic.%s" +## Automatic PBR material generation roughness map pattern +@export var roughness_map_pattern: String = "%s_roughness.%s" +## Automatic PBR material generation emission map pattern +@export var emission_map_pattern: String = "%s_emission.%s" +## Automatic PBR material generation ambient occlusion map pattern +@export var ao_map_pattern: String = "%s_ao.%s" +## Automatic PBR material generation height map pattern +@export var height_map_pattern: String = "%s_height.%s" +## Automatic PBR material generation ORM map pattern +@export var orm_map_pattern: String = "%s_orm.%s" + +## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes. [i]NOTE: Materials do not use the Default Material settings after saving.[/i] +@export var save_generated_materials: bool = true + +@export_category("UV Unwrap") + +## Texel size for UV2 unwrapping. +## Actual texel size is uv_unwrap_texel_size / inverse_scale_factor. A ratio of 1/16 is usually a good place to start with (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2). +## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good. +@export var uv_unwrap_texel_size: float = 2.0 + +@export_category("TrenchBroom") + +## If true, will organize Scene Tree using Trenchbroom Layers and Groups. Layers and Groups will be generated as [Node3D] nodes. +## All structural brushes will be moved out of the Layers and Groups and merged into the Worldspawn entity. +## Any Layers toggled to be omitted from export in TrenchBroom will not be built. +@export var use_trenchbroom_groups_hierarchy: bool = false diff --git a/addons/func_godot/src/map/func_godot_map_settings.gd.uid b/addons/func_godot/src/map/func_godot_map_settings.gd.uid new file mode 100644 index 00000000..7c749657 --- /dev/null +++ b/addons/func_godot/src/map/func_godot_map_settings.gd.uid @@ -0,0 +1 @@ +uid://bctwech0sq0kh diff --git a/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd b/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd new file mode 100644 index 00000000..2e775abe --- /dev/null +++ b/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd @@ -0,0 +1,274 @@ +@tool +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Builds a gamepack for NetRadiant Custom. +class_name NetRadiantCustomGamePackConfig +extends Resource + +## Button to export / update this gamepack's configuration in the NetRadiant Custom Gamepacks Folder. +@export var export_file: bool: + get: + return export_file + set(new_export_file): + if new_export_file != export_file: + if Engine.is_editor_hint(): + do_export_file() + +## Gamepack folder and file name. Must be lower case and must not contain special characters. +@export var gamepack_name : String = "func_godot" + +## Name of the game in NetRadiant Custom's gamepack list. +@export var game_name : String = "FuncGodot" + +## Directory path containing your maps, textures, shaders, etc... relative to your project directory. +@export var base_game_path : String = "" + +## FGD resource to include with this gamepack. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array. +@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") + +## [NetRadiantCustomShader] resources for shader file generation. +@export var netradiant_custom_shaders : Array[Resource] = [ + preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"), + preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres"), + preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres") +] + +## Supported texture file types. +@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"] + +## Supported model file types. +@export var model_types : PackedStringArray = ["glb", "gltf", "obj"] + +## Supported audio file types. +@export var sound_types : PackedStringArray = ["wav", "ogg"] + +## Default scale of textures in NetRadiant Custom. +@export var default_scale : String = "1.0" + +## Clip texture path that gets applied to weapclip and nodraw shaders. +@export var clip_texture: String = "textures/special/clip" + +## Skip texture path that gets applied to caulk and nodrawnonsolid shaders. +@export var skip_texture: String = "textures/special/skip" + +## Variables to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br] +## Each [String] key defines a variable name, and its corresponding [String] value as the literal command-line string to execute in place of this variable identifier[br][br] +## Entries may be referred to by key in [member default_build_menu_commands] values. +@export var default_build_menu_variables: Dictionary + +## Commands to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br] +## Keys, specified as a [String], define the build option name as you want it to appear in Radiant.[br][br] +## Values represent commands taken within each option.[br][br]They may be either a [String] or an +## [Array] of [String] elements that will be used as the full command-line text issued by each command [i]within[/i] +## its associated build option key. [br][br]They may reference entries in [member default_build_menu_variables] +## by using brackets: [code][variable key name][/code] +@export var default_build_menu_commands: Dictionary + +## Generates completed text for a .shader file. +func build_shader_text() -> String: + var shader_text: String = "" + for shader_res in netradiant_custom_shaders: + shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n" + for shader_attrib in (shader_res as NetRadiantCustomShader).shader_attributes: + shader_text += "\t" + shader_attrib + "\n" + shader_text += "}\n" + return shader_text + +## Generates completed text for a .gamepack file. +func build_gamepack_text() -> String: + var texturetypes_str: String = "" + for texture_type in texture_types: + texturetypes_str += texture_type + if texture_type != texture_types[-1]: + texturetypes_str += " " + + var modeltypes_str: String = "" + for model_type in model_types: + modeltypes_str += model_type + if model_type != model_types[-1]: + modeltypes_str += " " + + var soundtypes_str: String = "" + for sound_type in sound_types: + soundtypes_str += sound_type + if sound_type != sound_types[-1]: + soundtypes_str += " " + + var gamepack_text: String = """ + +""" + + return gamepack_text % [ + game_name, + game_name, + gamepack_name, + game_name, + gamepack_name, + base_game_path, + game_name, + game_name, + texturetypes_str, + modeltypes_str, + soundtypes_str, + default_scale, + clip_texture, + skip_texture, + clip_texture, + skip_texture + ] + +## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs. +func do_export_file() -> void: + if (FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String).is_empty(): + printerr("Skipping export: Map Editor Game Path not set in Project Configuration") + return + + var gamepacks_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER) as String + if gamepacks_folder.is_empty(): + printerr("Skipping export: No NetRadiant Custom gamepacks folder") + return + + # Make sure FGD file is set + if !fgd_file: + printerr("Skipping export: No FGD file") + return + + # Make sure we're actually in the NetRadiant Custom gamepacks folder + if DirAccess.open(gamepacks_folder + "/games") == null: + printerr("Skipping export: No \'games\' folder. Is this the NetRadiant Custom gamepacks folder?") + return + + # Create gamepack folders in case they do not exist + var gamepack_dir_paths: Array = [ + gamepacks_folder + "/" + gamepack_name + ".game", + gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path, + gamepacks_folder + "/" + gamepack_name + ".game/scripts" + ] + var err: Error + + for path in gamepack_dir_paths: + if DirAccess.open(path) == null: + print("Couldn't open " + path + ", creating...") + err = DirAccess.make_dir_recursive_absolute(path) + if err != OK: + printerr("Skipping export: Failed to create directory") + return + + var target_file_path: String + var file: FileAccess + + # .gamepack + target_file_path = gamepacks_folder + "/games/" + gamepack_name + ".game" + print("Exporting NetRadiant Custom Gamepack to ", target_file_path) + file = FileAccess.open(target_file_path, FileAccess.WRITE) + if file != null: + file.store_string(build_gamepack_text()) + file.close() + else: + printerr("Error: Could not modify " + target_file_path) + + # .shader + target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader" + print("Exporting NetRadiant Custom Shader to ", target_file_path) + file = FileAccess.open(target_file_path, FileAccess.WRITE) + if file != null: + file.store_string(build_shader_text()) + file.close() + else: + printerr("Error: Could not modify " + target_file_path) + + # shaderlist.txt + target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt" + print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path) + file = FileAccess.open(target_file_path, FileAccess.WRITE) + if file != null: + file.store_string(gamepack_name) + file.close() + else: + printerr("Error: Could not modify " + target_file_path) + + # default_build_menu.xml + target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml" + print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path) + file = FileAccess.open(target_file_path, FileAccess.WRITE) + + if file != null: + file.store_string("\n\n") + + for key in default_build_menu_variables.keys(): + if key is String: + if default_build_menu_variables[key] is String: + file.store_string('\t%s\n' % [key, default_build_menu_variables[key]]) + + else: + push_error( + "Variable key '%s' value '%s' is invalid type: %s; should be: String" % [ + key, default_build_menu_variables[key], + type_string(typeof(default_build_menu_variables[key])) + ]) + else: + push_error( + "Variable '%s' is an invalid key type: %s; should be: String" % [ + key, type_string(typeof(key)) + ]) + + + for key in default_build_menu_commands.keys(): + if key is String: + file.store_string('\t\n' % key) + + if default_build_menu_commands[key] is String: + file.store_string('\t\t%s\n\t\n' % default_build_menu_commands[key]) + + elif default_build_menu_commands[key] is Array: + for command in default_build_menu_commands[key]: + if command is String: + file.store_string('\t\t%s\n' % command) + else: + push_error("Build option '%s' has invalid command: %s with type: %s; should be: String" % [ + key, command, type_string(typeof(command)) + ]) + + file.store_string('\t\n') + + else: + push_error("Build option '%s' is an invalid type: %s; should be: String" % [ + key, type_string(typeof(key)) + ]) + + file.store_string("") + + # FGD + var export_fgd : FuncGodotFGDFile = fgd_file.duplicate() + export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.NET_RADIANT_CUSTOM, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path) + print("NetRadiant Custom Gamepack export complete\n") diff --git a/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid b/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid new file mode 100644 index 00000000..8c402b99 --- /dev/null +++ b/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid @@ -0,0 +1 @@ +uid://be0clu7ri4h10 diff --git a/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd b/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd new file mode 100644 index 00000000..8a0c3d89 --- /dev/null +++ b/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd @@ -0,0 +1,10 @@ +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom. +class_name NetRadiantCustomShader +extends Resource + +## Path to texture without extension, eg: `textures/special/clip`. +@export var texture_path: String + +## Array of shader properties to apply to faces using [member texture_path]. +@export var shader_attributes : Array[String] = ["qer_trans 0.4"] diff --git a/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid b/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid new file mode 100644 index 00000000..7772405d --- /dev/null +++ b/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid @@ -0,0 +1 @@ +uid://phswjcy6dexs diff --git a/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd b/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd new file mode 100644 index 00000000..f90399ee --- /dev/null +++ b/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd @@ -0,0 +1,331 @@ +@tool +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Defines a game in TrenchBroom to express a set of entity definitions and editor behaviors. +class_name TrenchBroomGameConfig +extends Resource + +## Keeps track of each individual version +enum GameConfigVersion { + Latest, + Version4, + Version8, + Version9 +} + +## Button to export / update this game's configuration and FGD file in the TrenchBroom Games Path. +@export var export_file: bool: + get: + return export_file + set(new_export_file): + if new_export_file != export_file: + if Engine.is_editor_hint(): + do_export_file() + +## Name of the game in TrenchBroom's game list. +@export var game_name : String = "FuncGodot" + +## Icon for TrenchBroom's game list. +@export var icon : Texture2D = preload("res://addons/func_godot/icon32.png") + +## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats. The `initialmap` key value is optional. +@export var map_formats: Array[Dictionary] = [ + { "format": "Valve", "initialmap": "initial_valve.map" }, + { "format": "Standard", "initialmap": "initial_standard.map" }, + { "format": "Quake2", "initialmap": "initial_quake2.map" }, + { "format": "Quake3" } +] + +@export_category("Textures") + +## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom. +@export var textures_root_folder: String = "textures" + +## Textures matching these patterns will be hidden from TrenchBroom. +@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"] + +## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information. +@export var palette_path: String = "textures/palette.lmp" + +@export_category("Entities") + +## FGD resource to include with this game. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array. +@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") + +## Scale expression that modifies the default display scale of entities in TrenchBroom. See the [**TrenchBroom Documentation**](https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities) for more information. +@export var entity_scale: String = "32" + +## Arrays containing the TrenchBroomTag resource type. +@export_category("Tags") + +## TrenchBroomTag resources that apply to brush entities. +@export var brush_tags : Array[Resource] = [] + +## TrenchBroomTag resources that apply to brush faces. +@export var brushface_tags : Array[Resource] = [ + preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"), + preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres"), + preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres") +] + +@export_category("Face Attributes") + +## Default scale of textures on new brushes and when UV scale is reset. +@export var default_uv_scale : Vector2 = Vector2(1, 1) + +@export_category("Compatibility") + +## Game configuration format compatible with the version of TrenchBroom being used. +@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest + +## Matches tag key enum to the String name used in .cfg +static func get_match_key(tag_match_type: int) -> String: + match tag_match_type: + TrenchBroomTag.TagMatchType.TEXTURE: + return "material" + TrenchBroomTag.TagMatchType.CLASSNAME: + return "classname" + _: + push_error("Tag match type %s is not valid" % [tag_match_type]) + return "ERROR" + +## Generates completed text for a .cfg file. +func build_class_text() -> String: + var map_formats_str : String = "" + for map_format in map_formats: + map_formats_str += "{ \"format\": \"" + map_format.format + "\"" + if map_format.has("initialmap"): + map_formats_str += ", \"initialmap\": \"" + map_format.initialmap + "\"" + if map_format != map_formats[-1]: + map_formats_str += " },\n\t\t" + else: + map_formats_str += " }" + + var texture_exclusion_patterns_str := "" + for tex_pattern in texture_exclusion_patterns: + texture_exclusion_patterns_str += "\"" + tex_pattern + "\"" + if tex_pattern != texture_exclusion_patterns[-1]: + texture_exclusion_patterns_str += ", " + + var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\"" + + var brush_tags_str = parse_tags(brush_tags) + var brushface_tags_str = parse_tags(brushface_tags) + var uv_scale_str = parse_default_uv_scale(default_uv_scale) + + var config_text : String = "" + match game_config_version: + GameConfigVersion.Latest, GameConfigVersion.Version8, GameConfigVersion.Version9: + config_text = get_game_config_v9v8_text() % [ + game_name, + map_formats_str, + textures_root_folder, + texture_exclusion_patterns_str, + palette_path, + fgd_filename_str, + entity_scale, + brush_tags_str, + brushface_tags_str, + uv_scale_str + ] + + GameConfigVersion.Version4: + config_text = get_game_config_v4_text() % [ + game_name, + map_formats_str, + textures_root_folder, + texture_exclusion_patterns_str, + palette_path, + fgd_filename_str, + entity_scale, + brush_tags_str, + brushface_tags_str, + uv_scale_str + ] + + _: + push_error("Unsupported Game Config Version!") + + return config_text + +## Converts brush, FuncGodotFace, and attribute tags into a .cfg-usable String. +func parse_tags(tags: Array) -> String: + var tags_str := "" + for brush_tag in tags: + if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size(): + continue + tags_str += "{\n" + tags_str += "\t\t\t\t\"name\": \"%s\",\n" % brush_tag.tag_name + var attribs_str := "" + for brush_tag_attrib in brush_tag.tag_attributes: + attribs_str += "\"%s\"" % brush_tag_attrib + if brush_tag_attrib != brush_tag.tag_attributes[-1]: + attribs_str += ", " + tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str + tags_str += "\t\t\t\t\"match\": \"%s\",\n" % get_match_key(brush_tag.tag_match_type) + tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern + if brush_tag.texture_name != "": + tags_str += ",\n" + tags_str += "\t\t\t\t\"material\": \"%s\"" % brush_tag.texture_name + tags_str += "\n" + tags_str += "\t\t\t}" + if brush_tag != tags[-1]: + tags_str += "," + if game_config_version > GameConfigVersion.Latest and game_config_version < GameConfigVersion.Version9: + tags_str = tags_str.replace("material", "texture") + return tags_str + +## Converts array of flags to .cfg String. +func parse_flags(flags: Array) -> String: + var flags_str := "" + for attrib_flag in flags: + flags_str += "{\n" + flags_str += "\t\t\t\t\"name\": \"%s\",\n" % attrib_flag.attrib_name + flags_str += "\t\t\t\t\"description\": \"%s\"\n" % attrib_flag.attrib_description + flags_str += "\t\t\t}" + if attrib_flag != flags[-1]: + flags_str += "," + return flags_str + +## Converts default uv scale vector to .cfg String. +func parse_default_uv_scale(texture_scale : Vector2) -> String: + var entry_str = "\"scale\": [{x}, {y}]" + return entry_str.format({ + "x": texture_scale.x, + "y": texture_scale.y + }) + +## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs. +func do_export_file() -> void: + var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String + if config_folder.is_empty(): + printerr("Skipping export: No TrenchBroom Game folder") + return + + # Make sure FGD file is set + if !fgd_file: + printerr("Skipping export: No FGD file") + return + + var config_dir := DirAccess.open(config_folder) + # Create config folder in case it does not exist + if config_dir == null: + print("Couldn't open directory, creating...") + var err := DirAccess.make_dir_recursive_absolute(config_folder) + if err != OK: + printerr("Skipping export: Failed to create directory") + return + + # Icon + var icon_path : String = config_folder + "/icon.png" + print("Exporting icon to ", icon_path) + var export_icon : Image = icon.get_image() + export_icon.resize(32, 32, Image.INTERPOLATE_LANCZOS) + export_icon.save_png(icon_path) + + # .cfg + var target_file_path: String = config_folder + "/GameConfig.cfg" + print("Exporting TrenchBroom Game Config to ", target_file_path) + var file = FileAccess.open(target_file_path, FileAccess.WRITE) + file.store_string(build_class_text()) + file.close() + + # FGD + var export_fgd : FuncGodotFGDFile = fgd_file.duplicate() + export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder) + print("TrenchBroom Game Config export complete\n") + +#region GameConfigDeclarations +func get_game_config_v4_text() -> String: + return """\ +{ + "version": 4, + "name": "%s", + "icon": "icon.png", + "fileformats": [ + %s + ], + "filesystem": { + "searchpath": ".", + "packageformat": { "extension": ".zip", "format": "zip" } + }, + "textures": { + "package": { "type": "directory", "root": "%s" }, + "format": { "extensions": ["jpg", "jpeg", "tga", "png", "D", "C"], "format": "image" }, + "excludes": [ %s ], + "palette": "%s", + "attribute": ["_tb_textures", "wad"] + }, + "entities": { + "definitions": [ %s ], + "defaultcolor": "0.6 0.6 0.6 1.0", + "modelformats": [ "bsp, mdl, md2" ], + "scale": %s + }, + "tags": { + "brush": [ + %s + ], + "brushface": [ + %s + ] + }, + "faceattribs": { + "defaults": { + %s + }, + "contentflags": [], + "surfaceflags": [] + } +} + """ + +func get_game_config_v9v8_text() -> String: + var config_text: String = """\ +{ + "version": 9, + "name": "%s", + "icon": "icon.png", + "fileformats": [ + %s + ], + "filesystem": { + "searchpath": ".", + "packageformat": { "extension": ".zip", "format": "zip" } + }, + "materials": { + "root": "%s", + "extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp", ".D", ".C"], + "excludes": [ %s ], + "palette": "%s", + "attribute": "wad" + }, + "entities": { + "definitions": [ %s ], + "defaultcolor": "0.6 0.6 0.6 1.0", + "scale": %s + }, + "tags": { + "brush": [ + %s + ], + "brushface": [ + %s + ] + }, + "faceattribs": { + "defaults": { + %s + }, + "contentflags": [], + "surfaceflags": [] + } +} + """ + + if game_config_version == GameConfigVersion.Version8: + config_text = config_text.replace(": 9,", ": 8,") + config_text = config_text.replace("material", "texture") + + return config_text + +#endregion diff --git a/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid b/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid new file mode 100644 index 00000000..c91d8267 --- /dev/null +++ b/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid @@ -0,0 +1 @@ +uid://cfpnvyygr4pb5 diff --git a/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd b/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd new file mode 100644 index 00000000..910df4fb --- /dev/null +++ b/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd @@ -0,0 +1,26 @@ +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options. This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot. +## See the TrenchBroom Documentation on [**Tags under the Game Configuration section**](https://trenchbroom.github.io/manual/latest/#game_configuration_files) and [**Special Bruch FuncGodotFace Types**](https://trenchbroom.github.io/manual/latest/#special_brush_face_types) for more information. +class_name TrenchBroomTag +extends Resource + +enum TagMatchType { + TEXTURE, ## Tag applies to any brush face with a texture matching the texture name. + CLASSNAME ## Tag applies to any brush entity with a class name matching the tag pattern. +} + +## Name to define this tag. Not used as the matching pattern. +@export var tag_name: String + +## The attributes applied to matching faces or brush entities. Only "_transparent" is supported in TrenchBroom, which makes matching faces or brush entities transparent. +@export var tag_attributes : Array[String] = ["transparent"] + +## Determines how the tag is matched. See [constant TagMatchType]. +@export var tag_match_type: TagMatchType + +## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options. +## [b]Example:[/b] [code]trigger_*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger_[/code] prefix. +@export var tag_pattern: String + +## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i]. +@export var texture_name: String diff --git a/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid b/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid new file mode 100644 index 00000000..348e3e5b --- /dev/null +++ b/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid @@ -0,0 +1 @@ +uid://cvwfhwn3pgig1 diff --git a/addons/func_godot/src/util/func_godot_local_config.gd b/addons/func_godot/src/util/func_godot_local_config.gd new file mode 100644 index 00000000..e81b3b40 --- /dev/null +++ b/addons/func_godot/src/util/func_godot_local_config.gd @@ -0,0 +1,142 @@ +@tool +@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") +## Local machine project wide settings. Can define global defaults for some FuncGodot properties. +## DO NOT CREATE A NEW RESOURCE! This resource works by saving a configuration file to your game's *user://* folder and pulling the properties from that config file rather than this resource. +## Use the premade `addons/func_godot/func_godot_local_config.tres` instead. +class_name FuncGodotLocalConfig +extends Resource + +enum PROPERTY { + FGD_OUTPUT_FOLDER, + TRENCHBROOM_GAME_CONFIG_FOLDER, + NETRADIANT_CUSTOM_GAMEPACKS_FOLDER, + MAP_EDITOR_GAME_PATH, + GAME_PATH_MODELS_FOLDER, + DEFAULT_INVERSE_SCALE +} + +@export var export_func_godot_settings: bool: set = _save_settings +@export var reload_func_godot_settings: bool = false : + set(value): + _load_settings() + +const CONFIG_PROPERTIES: Array[Dictionary] = [ + { + "name": "fgd_output_folder", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_STRING, + "hint": PROPERTY_HINT_GLOBAL_DIR, + "func_godot_type": PROPERTY.FGD_OUTPUT_FOLDER + }, + { + "name": "trenchbroom_game_config_folder", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_STRING, + "hint": PROPERTY_HINT_GLOBAL_DIR, + "func_godot_type": PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER + }, + { + "name": "netradiant_custom_gamepacks_folder", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_STRING, + "hint": PROPERTY_HINT_GLOBAL_DIR, + "func_godot_type": PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER + }, + { + "name": "map_editor_game_path", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_STRING, + "hint": PROPERTY_HINT_GLOBAL_DIR, + "func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH + }, + { + "name": "game_path_models_folder", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_STRING, + "func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER + }, + { + "name": "default_inverse_scale_factor", + "usage": PROPERTY_USAGE_EDITOR, + "type": TYPE_FLOAT, + "func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE + } +] + +var settings_dict: Dictionary +var loaded := false + +static func get_setting(name: PROPERTY) -> Variant: + var settings = load("res://addons/func_godot/func_godot_local_config.tres") + if not settings.loaded: + settings._load_settings() + return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant + +func _get_property_list() -> Array: + return CONFIG_PROPERTIES.duplicate() + +func _get(property: StringName) -> Variant: + var config = _get_config_property(property) + if config == null and not config is Dictionary: + return null + _try_loading() + return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type'])) + +func _set(property: StringName, value: Variant) -> bool: + var config = _get_config_property(property) + if config == null and not config is Dictionary: + return false + settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value + return true + +func _get_default_value(type) -> Variant: + match type: + TYPE_STRING: return '' + TYPE_INT: return 0 + TYPE_FLOAT: return 0.0 + TYPE_BOOL: return false + TYPE_VECTOR2: return Vector2.ZERO + TYPE_VECTOR3: return Vector3.ZERO + TYPE_ARRAY: return [] + TYPE_DICTIONARY: return {} + push_error("Invalid setting type. Returning null") + return null + +func _get_config_property(name: StringName) -> Variant: + for config in CONFIG_PROPERTIES: + if config['name'] == name: + return config + return null + +func _load_settings() -> void: + loaded = true + var path = _get_path() + if not FileAccess.file_exists(path): + return + var settings = FileAccess.get_file_as_string(path) + settings_dict = {} + if not settings or settings.is_empty(): + return + settings = JSON.parse_string(settings) + for key in settings.keys(): + settings_dict[key] = settings[key] + notify_property_list_changed() + +func _try_loading() -> void: + if not loaded: + _load_settings() + +func _save_settings(_s = null) -> void: + if settings_dict.size() == 0: + return + var path = _get_path() + var file = FileAccess.open(path, FileAccess.WRITE) + var json = JSON.stringify(settings_dict) + file.store_line(json) + loaded = false + print("Saved settings to ", path) + +func _get_path() -> String: + var application_name: String = ProjectSettings.get('application/config/name') + application_name = application_name.replace(" ", "_") + return 'user://' + application_name + '_FuncGodotConfig.json' diff --git a/addons/func_godot/src/util/func_godot_local_config.gd.uid b/addons/func_godot/src/util/func_godot_local_config.gd.uid new file mode 100644 index 00000000..c09a2495 --- /dev/null +++ b/addons/func_godot/src/util/func_godot_local_config.gd.uid @@ -0,0 +1 @@ +uid://csppcqp1pieqe diff --git a/addons/func_godot/src/util/func_godot_texture_loader.gd b/addons/func_godot/src/util/func_godot_texture_loader.gd new file mode 100644 index 00000000..81d7f4b6 --- /dev/null +++ b/addons/func_godot/src/util/func_godot_texture_loader.gd @@ -0,0 +1,187 @@ +class_name FuncGodotTextureLoader + +enum PBRSuffix { + ALBEDO, + NORMAL, + METALLIC, + ROUGHNESS, + EMISSION, + AO, + HEIGHT, + ORM +} + +# Suffix string / Godot enum / StandardMaterial3D property +const PBR_SUFFIX_NAMES: Dictionary = { + PBRSuffix.ALBEDO: 'albedo', + PBRSuffix.NORMAL: 'normal', + PBRSuffix.METALLIC: 'metallic', + PBRSuffix.ROUGHNESS: 'roughness', + PBRSuffix.EMISSION: 'emission', + PBRSuffix.AO: 'ao', + PBRSuffix.HEIGHT: 'height', + PBRSuffix.ORM: 'orm' +} + +const PBR_SUFFIX_PATTERNS: Dictionary = { + PBRSuffix.ALBEDO: '%s_albedo.%s', + PBRSuffix.NORMAL: '%s_normal.%s', + PBRSuffix.METALLIC: '%s_metallic.%s', + PBRSuffix.ROUGHNESS: '%s_roughness.%s', + PBRSuffix.EMISSION: '%s_emission.%s', + PBRSuffix.AO: '%s_ao.%s', + PBRSuffix.HEIGHT: '%s_height.%s', + PBRSuffix.ORM: '%s_orm.%s' +} + +var PBR_SUFFIX_TEXTURES: Dictionary = { + PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO, + PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL, + PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC, + PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS, + PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION, + PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION, + PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP, + PBRSuffix.ORM: ORMMaterial3D.TEXTURE_ORM +} + +const PBR_SUFFIX_PROPERTIES: Dictionary = { + PBRSuffix.NORMAL: 'normal_enabled', + PBRSuffix.EMISSION: 'emission_enabled', + PBRSuffix.AO: 'ao_enabled', + PBRSuffix.HEIGHT: 'heightmap_enabled', +} + +var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new() +var texture_wad_resources: Array = [] + +# Overrides +func _init(new_map_settings: FuncGodotMapSettings) -> void: + map_settings = new_map_settings + load_texture_wad_resources() + +# Business Logic +func load_texture_wad_resources() -> void: + texture_wad_resources.clear() + for texture_wad in map_settings.texture_wads: + if texture_wad and not texture_wad in texture_wad_resources: + texture_wad_resources.append(texture_wad) + +func load_textures(texture_list: Array) -> Dictionary: + var texture_dict: Dictionary = {} + for texture_name in texture_list: + texture_dict[texture_name] = load_texture(texture_name) + return texture_dict + +func load_texture(texture_name: String) -> Texture2D: + # Load albedo texture if it exists + for texture_extension in map_settings.texture_file_extensions: + var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension] + if ResourceLoader.exists(texture_path, "Texture2D") or ResourceLoader.exists(texture_path + ".import", "Texture2D"): + return load(texture_path) as Texture2D + + var texture_name_lower: String = texture_name.to_lower() + for texture_wad in texture_wad_resources: + if texture_name_lower in texture_wad.textures: + return texture_wad.textures[texture_name_lower] + + return load("res://addons/func_godot/textures/default_texture.png") as Texture2D + +func create_materials(texture_list: Array) -> Dictionary: + var texture_materials: Dictionary = {} + #prints("TEXLI", texture_list) + for texture in texture_list: + texture_materials[texture] = create_material(texture) + return texture_materials + +func create_material(texture_name: String) -> Material: + # Autoload material if it exists + var material_dict: Dictionary = {} + + var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension] + if not material_path in material_dict and (FileAccess.file_exists(material_path) or FileAccess.file_exists(material_path + ".remap")): + var loaded_material: Material = load(material_path) + if loaded_material: + material_dict[material_path] = loaded_material + + # If material already exists, use it + if material_path in material_dict: + return material_dict[material_path] + + var material: Material = null + + if map_settings.default_material: + material = map_settings.default_material.duplicate() + else: + material = StandardMaterial3D.new() + var texture: Texture2D = load_texture(texture_name) + if not texture: + return material + + if material is StandardMaterial3D: + material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture) + elif material is ShaderMaterial && map_settings.default_material_albedo_uniform != "": + material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture) + elif material is ORMMaterial3D: + material.set_texture(ORMMaterial3D.TEXTURE_ALBEDO, texture) + + var pbr_textures : Dictionary = get_pbr_textures(texture_name) + + for pbr_suffix in PBRSuffix.values(): + var suffix: int = pbr_suffix + var tex: Texture2D = pbr_textures[suffix] + if tex: + if material is ShaderMaterial: + material = StandardMaterial3D.new() + material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture) + var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else "" + if(enable_prop != ""): + material.set(enable_prop, true) + material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex) + + material_dict[material_path] = material + + if (map_settings.save_generated_materials and material + and texture_name != map_settings.clip_texture + and texture_name != map_settings.skip_texture + and texture.resource_path != "res://addons/func_godot/textures/default_texture.png"): + ResourceSaver.save(material, material_path) + + return material + +# PBR texture fetching +func get_pbr_suffix_pattern(suffix: int) -> String: + if not suffix in PBR_SUFFIX_NAMES: + return '' + + var pattern_setting: String = "%s_map_pattern" % [PBR_SUFFIX_NAMES[suffix]] + if pattern_setting in map_settings: + return map_settings.get(pattern_setting) + + return PBR_SUFFIX_PATTERNS[suffix] + +func get_pbr_texture(texture: String, suffix: PBRSuffix) -> Texture2D: + var texture_comps: PackedStringArray = texture.split('/') + if texture_comps.size() == 0: + return null + + for texture_extension in map_settings.texture_file_extensions: + var path: String = "%s/%s/%s" % [ + map_settings.base_texture_dir, + '/'.join(texture_comps), + get_pbr_suffix_pattern(suffix) % [ + texture_comps[-1], + texture_extension + ] + ] + + if(FileAccess.file_exists(path)): + return load(path) as Texture2D + + return null + +func get_pbr_textures(texture_name: String) -> Dictionary: + var pbr_textures: Dictionary = {} + for pbr_suffix in PBRSuffix.values(): + pbr_textures[pbr_suffix] = get_pbr_texture(texture_name, pbr_suffix) + return pbr_textures diff --git a/addons/func_godot/src/util/func_godot_texture_loader.gd.uid b/addons/func_godot/src/util/func_godot_texture_loader.gd.uid new file mode 100644 index 00000000..08ed5bfb --- /dev/null +++ b/addons/func_godot/src/util/func_godot_texture_loader.gd.uid @@ -0,0 +1 @@ +uid://c0r8ajf4k061i diff --git a/addons/func_godot/src/util/func_godot_util.gd b/addons/func_godot/src/util/func_godot_util.gd new file mode 100644 index 00000000..7f040a99 --- /dev/null +++ b/addons/func_godot/src/util/func_godot_util.gd @@ -0,0 +1,40 @@ +## General-purpose utility functions namespaced to FuncGodot for compatibility +class_name FuncGodotUtil + +## Print debug messages. True to print, false to ignore +const DEBUG : bool = true + +## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`. +static func debug_print(msg) -> void: + if(DEBUG): + print(msg) + +## Return a string that corresponds to the current OS's newline control character(s) +static func newline() -> String: + if OS.get_name() == "Windows": + return "\r\n" + else: + return "\n" + +## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list] +static func category_dict(name: String) -> Dictionary: + return property_dict(name, TYPE_STRING, -1, "", PROPERTY_USAGE_CATEGORY) + +## Creates a property with name and type from [enum @GlobalScope.Variant.Type]. +## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags]. +static func property_dict(name: String, type: int, hint: int = -1, hint_string: String = "", usage: int = -1) -> Dictionary: + var dict := { + 'name': name, + 'type': type + } + + if hint != -1: + dict['hint'] = hint + + if hint_string != "": + dict['hint_string'] = hint_string + + if usage != -1: + dict['usage'] = usage + + return dict diff --git a/addons/func_godot/src/util/func_godot_util.gd.uid b/addons/func_godot/src/util/func_godot_util.gd.uid new file mode 100644 index 00000000..54a3bbe1 --- /dev/null +++ b/addons/func_godot/src/util/func_godot_util.gd.uid @@ -0,0 +1 @@ +uid://bm0rqimmm2870 diff --git a/addons/func_godot/textures/default_material.tres b/addons/func_godot/textures/default_material.tres new file mode 100644 index 00000000..30c0e59e --- /dev/null +++ b/addons/func_godot/textures/default_material.tres @@ -0,0 +1,8 @@ +[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cvex6toty8yn7"] + +[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://addons/func_godot/textures/default_texture.png" id="1_ncj77"] + +[resource] +albedo_texture = ExtResource("1_ncj77") +metallic_specular = 0.0 +texture_filter = 2 diff --git a/addons/func_godot/textures/default_texture.png b/addons/func_godot/textures/default_texture.png new file mode 100644 index 00000000..09c7fffd --- /dev/null +++ b/addons/func_godot/textures/default_texture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:908eba4aef4a7bd13bab94ee2d825787e3c9df54b0785e72f06f695c282b757c +size 1044 diff --git a/addons/func_godot/textures/default_texture.png.import b/addons/func_godot/textures/default_texture.png.import new file mode 100644 index 00000000..187d2527 --- /dev/null +++ b/addons/func_godot/textures/default_texture.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cyg2snr1w5xw5" +path="res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/textures/default_texture.png" +dest_files=["res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex"] + +[params] + +compress/mode=3 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/func_godot/textures/special/clip.png b/addons/func_godot/textures/special/clip.png new file mode 100644 index 00000000..0987f8e7 --- /dev/null +++ b/addons/func_godot/textures/special/clip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05b888c861f4a50d1d4a43f53d7a651be188a0a04a33dec3fef060c7470b3993 +size 1124 diff --git a/addons/func_godot/textures/special/clip.png.import b/addons/func_godot/textures/special/clip.png.import new file mode 100644 index 00000000..a97dba7c --- /dev/null +++ b/addons/func_godot/textures/special/clip.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhmu0toe1itnr" +path="res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/textures/special/clip.png" +dest_files=["res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex"] + +[params] + +compress/mode=3 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/func_godot/textures/special/origin.png b/addons/func_godot/textures/special/origin.png new file mode 100644 index 00000000..2d474b3c --- /dev/null +++ b/addons/func_godot/textures/special/origin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc0d99426879920af8c7d41dadc3279960c461c21b95a17513b25e52846257d7 +size 239 diff --git a/addons/func_godot/textures/special/origin.png.import b/addons/func_godot/textures/special/origin.png.import new file mode 100644 index 00000000..ec4df623 --- /dev/null +++ b/addons/func_godot/textures/special/origin.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dutip72dl002r" +path="res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/textures/special/origin.png" +dest_files=["res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/func_godot/textures/special/skip.png b/addons/func_godot/textures/special/skip.png new file mode 100644 index 00000000..23a3f13d --- /dev/null +++ b/addons/func_godot/textures/special/skip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:762e2c8d06ec1b3719e4abd875a4a390259b317455a161a1fd9b5640d4ed3664 +size 1138 diff --git a/addons/func_godot/textures/special/skip.png.import b/addons/func_godot/textures/special/skip.png.import new file mode 100644 index 00000000..b9a95b45 --- /dev/null +++ b/addons/func_godot/textures/special/skip.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bk5oo263y3u7w" +path="res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/func_godot/textures/special/skip.png" +dest_files=["res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex"] + +[params] + +compress/mode=3 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/textures/Floors b/textures/Floors new file mode 120000 index 00000000..714f2623 --- /dev/null +++ b/textures/Floors @@ -0,0 +1 @@ +../ExternalMaterial/HexGrid/Singles/Floors \ No newline at end of file diff --git a/textures/Various b/textures/Various new file mode 120000 index 00000000..cbfe6057 --- /dev/null +++ b/textures/Various @@ -0,0 +1 @@ +../ExternalMaterial/HexGrid/Singles/Various \ No newline at end of file diff --git a/textures/Walls b/textures/Walls new file mode 120000 index 00000000..010e5bbc --- /dev/null +++ b/textures/Walls @@ -0,0 +1 @@ +../ExternalMaterial/HexGrid/Singles/Walls \ No newline at end of file