├── .gitattributes
├── .gitignore
├── Boot.gd
├── BootScreen.gd
├── BootScreen.tscn
├── CameraAnchor.gd
├── CityView
├── CityScene
│ ├── CameraAnchor3D.gd
│ ├── City.gd
│ └── City.tscn
├── ClassDefinitions
│ ├── NetGraphEdge.gd
│ ├── NetGraphNode.gd
│ ├── NetTile.gd
│ ├── NeworkGraph.gd
│ └── TransitTile.gd
└── Meshes
│ ├── Terrain.gd
│ ├── TestS3D.gd
│ ├── TransitTiles.gd
│ └── WaterPlane.gd
├── Core.gd
├── DATExplorer
├── DATExplorer.gd
├── DATExplorer.tscn
├── Filter.tscn
├── SubfilePreview.gd
└── SubfilePreview.tscn
├── Graphics Rules.sgr.backup
├── README.md
├── Radio
└── Stations
│ └── RadioPlayer.gd
├── Region.gd
├── Region.tscn
├── RegionGrid.gd
├── RegionUI
├── AudioSettingsButton.gd
├── BrowseRegionsButton.gd
├── BrowseScreenshotsButton.gd
├── CityThumbnailArea.gd
├── Compass.gd
├── DeleteRegionButton.gd
├── DisplaySettingsButton.gd
├── ExitGameButton.gd
├── GameSettingsButton.gd
├── InternetButton.gd
├── NameAndPopulation.gd
├── NewRegionButton.gd
├── PopulationIndicator.gd
├── RegionCityView.gd
├── RegionCityView.tscn
├── RegionManagementButton.gd
├── RegionNameDisplay.gd
├── RegionSubmenu.gd
├── SatelliteViewRadioButton.gd
├── SaveScreenshotButton.gd
├── ShowBordersCheckbox.gd
├── ShowNamesCheckbox.gd
├── Thumbnail.gd
├── TopBarButtons.gd
├── TopBarDecoration.gd
├── TopBarSettingsButton.gd
├── TopBarSettingsButtonContainer.gd
├── TopBarSettingsMenu.gd
├── TransportationViewRadioButton.gd
└── ViewOptionsContainer.gd
├── RegionViewCityThumbnail.gd
├── SC4City__WriteRegionViewThumbnail.gd
├── SC4ReadRegionalCity.gd
├── SC4UISubfile.gd
├── SubfilePreview.tscn
├── Terrain.gd
├── addons
└── dbpf
│ ├── CURSubfile.gd
│ ├── CUR_Entry.gd
│ ├── DBDF.gd
│ ├── DBDFEntry.gd
│ ├── DBPF.gd
│ ├── DBPFPlugin.gd
│ ├── DBPFSubfile.gd
│ ├── ExemplarSubfile.gd
│ ├── FSHSubfile.gd
│ ├── FSH_Entry.gd
│ ├── GZWin.gd
│ ├── GZWinBMP.gd
│ ├── GZWinBtn.gd
│ ├── GZWinFlatRect.gd
│ ├── GZWinGen.gd
│ ├── GZWinText.gd
│ ├── INISubfile.gd
│ ├── ImageSubfile.gd
│ ├── LTEXTSubfile.gd
│ ├── RULSubfile.gd
│ ├── S3DSubfile.gd
│ ├── S3D_Group.gd
│ ├── SubfileIndex.gd
│ ├── SubfileTGI.gd
│ ├── dbpf.png
│ ├── dbpf.png.import
│ └── plugin.cfg
├── cSTETerrain__SaveAltitudes.gd
├── dev_notes
└── known_tgis
│ └── city - god mode.txt
├── exemplar_types.dict
├── ini.gd
├── project.godot
├── splash.png
├── splash.png.import
└── utils
├── debug_ui
└── loading_icon_spinning.tres
├── debug_utils.gd
└── logger.gd
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.cpp text
7 | *.c text
8 | *.h text
9 | *.gd text
10 | *.cs text
11 |
12 | # Declare files that will always have CRLF line endings on checkout.
13 | *.sln text eol=crlf
14 |
15 | # Denote all files that are truly binary and should not be modified.
16 | *.png binary
17 | *.jpg binary
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Plugin Specific ignores
2 | default_env.tres
3 | icon.png
4 | icon.png.import
5 | scn/
6 | override.cfg
7 |
8 | # Godot-specific ignores
9 | .import/
10 | export.cfg
11 | export_presets.cfg
12 |
13 | # Mono-specific ignores
14 | .mono/
15 | data_*/
16 |
17 | #Simcity4 data
18 | Regions/*
19 | EULA/*
20 | fontconfig/*
21 | Fonts/*
22 | Plugins/*
23 | Radio/Stations/Mayor/
24 | Radio/Stations/Region/
25 | ReadMe/*
26 | Sku_Data/*
27 | Support/*
28 | *.vdf
29 | *filelist.txt
30 | *.exe
31 | *.ico
32 | *.dat
33 | readme.txt
34 | *.sgr
35 | *.dll
36 | *.ini
37 | *.DAT
38 |
39 | # Development files
40 | .dev/*
41 |
42 | .github
43 | *.Backup
44 | *.import
45 | *.png
46 | *.fx
47 | *.fxh
48 | *.log
49 | Apps/LUURT-config-log.txt
50 | Apps/dgVoodoo.conf
51 | tropod_Properties.xml
52 |
--------------------------------------------------------------------------------
/Boot.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | var INI_location = "./original_data_files/SimCity 4.ini"
4 |
5 | var current_city : DBPF
6 |
--------------------------------------------------------------------------------
/BootScreen.gd:
--------------------------------------------------------------------------------
1 | extends Control
2 |
3 | """
4 | Once everything is loaded the it changes to Region scene or the DAT explorer
5 | """
6 |
7 |
8 | var config
9 |
10 | var loading_thread : Thread
11 | var dat_files : Array = [
12 | "SimCity 4.ini",
13 | "Sound.dat",
14 | "Intro.dat",
15 | "SimCity_1.dat",
16 | "SimCity_2.dat",
17 | "SimCity_3.dat",
18 | "SimCity_4.dat",
19 | "SimCity_5.dat",
20 | "EP1.dat",]
21 |
22 | func _ready():
23 | #_generate_types_dict_from_XML()
24 | config = INI.new("user://config.ini")
25 | if config.sections.size() > 0:
26 | Core.game_dir = config.sections["paths"]["sc4_files"]
27 | else:
28 | $dialog.popup_centered(get_viewport_rect().size / 2)
29 | yield($dialog, "popup_hide")
30 | config.sections["paths"] = {}
31 | config.sections["paths"]["sc4_files"] = Core.game_dir
32 | config.save_file()
33 |
34 | #TODO check if files exist in current Core.game_dir
35 | var dir = Directory.new()
36 | var dir_complete = true
37 | while not dir_complete:
38 | if dir.open(Core.game_dir) == OK:
39 | dir.list_dir_begin()
40 | var files = []
41 | var file_name = dir.get_next()
42 | while file_name != "":
43 | files.append(file_name)
44 | file_name = dir.get_next()
45 | for dat in dat_files:
46 | if "/" in dat:
47 | var folders = dat.split('/')
48 | var folder_dir = ""
49 | for folder in range(len(folders)-1):
50 | folder_dir += ("/" + folders[folder])
51 | var file_n = folders[-1]
52 | var subdir = Directory.new()
53 | print(Core.game_dir+folder_dir)
54 | if subdir.open(Core.game_dir+folder_dir) == OK:
55 | subdir.list_dir_begin()
56 | var subfile_name = subdir.get_next()
57 | var found = false
58 | while subfile_name != "":
59 | if subfile_name == file_n:
60 | found = true
61 | break
62 | subfile_name = subdir.get_next()
63 | if not found:
64 | dir_complete = false
65 | print(dat, "not found")
66 | else:
67 | dir_complete = false
68 | print(dat, "not found")
69 |
70 | elif not files.has(dat):
71 | dir_complete = false
72 | print(dat, "not found")
73 | else:
74 | dir_complete = false
75 | if not dir_complete:
76 | $dialog.window_title = "dir was incomplete, select the SC4 installation folder"
77 | $dialog.popup_centered(get_viewport_rect().size / 2)
78 | yield($dialog, "popup_hide")
79 | print("todo: store path in cfg.ini")
80 | config.sections["paths"] = {}
81 | config.sections["paths"]["sc4_files"] = Core.game_dir
82 | config.save_file()
83 | $dialog.deselect_items()
84 | $LoadProgress.value = 0
85 | loading_thread = Thread.new()
86 | Logger.info("Loading OpenSC4...")
87 | Logger.info("Using %s as game data folder" % Core.game_dir)
88 | # Would be nice to start multiple threads here not only one
89 | var err = loading_thread.start(self, 'load_DATs')
90 | if err != OK:
91 | Logger.error("Error starting thread: " % err)
92 | return
93 |
94 | func _exit_tree():
95 | loading_thread.wait_to_finish()
96 |
97 | func load_DATs():
98 | for dat_file in dat_files :
99 | load_single_DAT(dat_file)
100 | finish_loading()
101 |
102 | func finish_loading():
103 | Logger.info("DBPF files loaded")
104 | $NextScene.visible = true
105 |
106 | func load_single_DAT(dat_file : String):
107 | var src = Core.game_dir + "/" + dat_file
108 | $CurrentFileLabel.text = "Loading: %s" % src
109 | var dbpf = DBPF.new(src)
110 | #dbpf.DEBUG_show_all_subfiles_to_file(dat_file)
111 | Core.add_dbpf(dbpf)
112 | $LoadProgress.value += 100.0/len(dat_files)
113 |
114 |
115 | func _on_dialog_confirmed():
116 | Core.game_dir = $dialog.current_dir
117 |
118 | func _on_DATExplorerButton_pressed():
119 | print("here")
120 | var err = get_tree().change_scene("res://DATExplorer/DATExplorer.tscn")
121 | if err != OK:
122 | Logger.error("Error: %s" % err)
123 |
124 | func _on_GameButton_pressed():
125 | var err = get_tree().change_scene("res://Region.tscn")
126 | if err != OK:
127 | Logger.error("Error: %s" % err)
128 |
129 |
130 |
--------------------------------------------------------------------------------
/BootScreen.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=3 format=2]
2 |
3 | [ext_resource path="res://BootScreen.gd" type="Script" id=1]
4 | [ext_resource path="res://splash.png" type="Texture" id=2]
5 |
6 | [node name="BootScreen" type="Control"]
7 | anchor_right = 1.0
8 | anchor_bottom = 1.0
9 | script = ExtResource( 1 )
10 |
11 | [node name="dialog" type="FileDialog" parent="."]
12 | margin_left = 228.0
13 | margin_top = 87.0
14 | margin_right = 1045.0
15 | margin_bottom = 649.0
16 | rect_min_size = Vector2( 150, 52.5 )
17 | popup_exclusive = true
18 | window_title = "set the SimCity 4 directory"
19 | mode_overrides_title = false
20 | mode = 2
21 | access = 2
22 | show_hidden_files = true
23 |
24 | [node name="LoadProgress" type="ProgressBar" parent="."]
25 | anchor_left = 0.25
26 | anchor_top = 0.514
27 | anchor_right = 0.75
28 | anchor_bottom = 0.55
29 | margin_left = -10.0
30 | margin_top = 219.92
31 | margin_right = -10.0
32 | margin_bottom = 219.92
33 |
34 | [node name="Label" type="Label" parent="."]
35 | margin_left = 310.0
36 | margin_top = 568.0
37 | margin_right = 433.0
38 | margin_bottom = 582.0
39 | text = "Loading OpenSC4..."
40 |
41 | [node name="CurrentFileLabel" type="Label" parent="."]
42 | margin_left = 310.0
43 | margin_top = 631.0
44 | margin_right = 350.0
45 | margin_bottom = 645.0
46 |
47 | [node name="background" type="Sprite" parent="."]
48 | position = Vector2( 636, 366 )
49 | scale = Vector2( 0.6625, 0.675 )
50 | z_index = -1
51 | texture = ExtResource( 2 )
52 |
53 | [node name="NextScene" type="Panel" parent="."]
54 | visible = false
55 | anchor_left = 0.5
56 | anchor_top = 0.5
57 | anchor_right = 0.5
58 | anchor_bottom = 0.5
59 | margin_left = -381.0
60 | margin_top = -139.0
61 | margin_right = 381.0
62 | margin_bottom = 139.0
63 |
64 | [node name="VBoxContainer" type="VBoxContainer" parent="NextScene"]
65 | anchor_left = 0.5
66 | anchor_top = 0.5
67 | anchor_right = 0.5
68 | anchor_bottom = 0.5
69 | margin_left = -122.0
70 | margin_top = -19.0
71 | margin_right = -122.0
72 | margin_bottom = -19.0
73 |
74 | [node name="Label" type="Label" parent="NextScene/VBoxContainer"]
75 | margin_right = 244.0
76 | margin_bottom = 14.0
77 | text = "Game files loaded"
78 | align = 1
79 |
80 | [node name="HBoxContainer" type="HBoxContainer" parent="NextScene/VBoxContainer"]
81 | margin_top = 18.0
82 | margin_right = 244.0
83 | margin_bottom = 38.0
84 |
85 | [node name="GameButton" type="Button" parent="NextScene/VBoxContainer/HBoxContainer"]
86 | margin_right = 96.0
87 | margin_bottom = 20.0
88 | text = "Launch game"
89 |
90 | [node name="DATExplorerButton" type="Button" parent="NextScene/VBoxContainer/HBoxContainer"]
91 | margin_left = 100.0
92 | margin_right = 244.0
93 | margin_bottom = 20.0
94 | text = "Launch DAT explorer"
95 |
96 | [connection signal="confirmed" from="dialog" to="." method="_on_dialog_confirmed"]
97 | [connection signal="dir_selected" from="dialog" to="." method="_on_dialog_dir_selected"]
98 | [connection signal="pressed" from="NextScene/VBoxContainer/HBoxContainer/GameButton" to="." method="_on_GameButton_pressed"]
99 | [connection signal="pressed" from="NextScene/VBoxContainer/HBoxContainer/DATExplorerButton" to="." method="_on_DATExplorerButton_pressed"]
100 |
--------------------------------------------------------------------------------
/CameraAnchor.gd:
--------------------------------------------------------------------------------
1 | extends KinematicBody2D
2 |
3 | var velocity = Vector2(0, 0)
4 | var viewport = 0
5 | var margin_w = 0
6 | var margin_h = 0
7 | var move = Vector2(0, 0)
8 | var mouse_right_click_origin = Vector2(0, 0)
9 | var mouse_position = Vector2(0, 0)
10 |
11 | #Should move one screen width every 4 seconds
12 | #var move_speed = viewport.size.x / 4
13 | var move_speed = 16834 # just manually setting it idk
14 |
15 | func _ready():
16 | pass
17 |
18 | func _input(event):
19 | viewport = self.get_viewport()
20 | margin_w = viewport.size.x / 15
21 | margin_h = viewport.size.y / 15
22 |
23 | if event is InputEventMouseMotion:
24 | if event.position.x < margin_w:
25 | move.x = -1
26 | elif event.position.x > viewport.size.x - margin_w:
27 | move.x = 1
28 |
29 | if event.position.y < margin_h:
30 | move.y = -1
31 | elif event.position.y > viewport.size.y - margin_h:
32 | move.y = 1
33 |
34 | if Input.is_action_pressed("camera_up"):
35 | move.y = -1
36 | elif Input.is_action_pressed("camera_down"):
37 | move.y = 1
38 |
39 | if Input.is_action_pressed("camera_left"):
40 | move.x = -1
41 | elif Input.is_action_pressed("camera_right"):
42 | move.x = 1
43 |
44 | # `normalized() * move_speed` so no fast diagonal movement
45 | self.velocity = move.normalized() * move_speed
46 |
47 | # reset variables
48 | move.x = 0
49 | move.y = 0
50 |
51 | func right_click_movement():
52 | if Input.is_action_just_pressed("camera_right_click"):
53 | mouse_right_click_origin = get_local_mouse_position()
54 |
55 | if Input.is_action_pressed("camera_right_click"):
56 | mouse_position = get_local_mouse_position()
57 | move = mouse_position - mouse_right_click_origin
58 |
59 | self.velocity = move.normalized() * move_speed * move.length()/256
60 |
61 | move.x = 0
62 | move.y = 0
63 |
64 | func _process(delta):
65 | right_click_movement()
66 | self.move_and_slide(self.velocity * delta)
67 |
--------------------------------------------------------------------------------
/CityView/CityScene/CameraAnchor3D.gd:
--------------------------------------------------------------------------------
1 | extends KinematicBody
2 |
3 | var zoom = 1
4 | var zoom_list = [292, 146, 73, 32, 16, 8]
5 | var elevations = [60, 55, 50, 45, 45, 45]
6 | var AZIMUTH = deg2rad(67.5)
7 | var rotated = 2
8 | var boot = true
9 | var velocity = Vector3(0, 0, 0)
10 | var hold_r = []
11 |
12 | func _ready():
13 | self.transform.origin = Vector3(self.transform.origin.x, get_parent().WATER_HEIGHT, self.transform.origin.z)
14 | _set_view()
15 | $Camera.set_znear(-200.0)
16 | pass
17 |
18 | func _input(event):
19 | var viewport = self.get_viewport()
20 | var margin_w = viewport.size.x / 15
21 | var margin_h = viewport.size.y / 15
22 | var move = Vector3(0, 0, 0)
23 | var camera_forward = Vector3(self.transform.basis.z.x, 0, self.transform.basis.z.z)
24 | var camera_left = Vector3(self.transform.basis.x.x, 0, self.transform.basis.x.z)
25 |
26 | # Should move one screen width every 5 seconds
27 | var move_vel = $Camera.size
28 | if event is InputEventMouseMotion:
29 | if event.position.x < margin_w:
30 | move -= camera_left
31 | elif event.position.x > viewport.size.x - margin_w:
32 | move += camera_left
33 |
34 | if event.position.y < margin_h:
35 | move -= camera_forward
36 | elif event.position.y > viewport.size.y - margin_h:
37 | move += camera_forward
38 |
39 | self.velocity = move.normalized() * move_vel
40 | #print($Camera.project_position(Vector2(0.5, 0.5), 0.5))
41 | elif event is InputEventMouseButton:
42 | if event.is_pressed():
43 | if event.button_index == BUTTON_WHEEL_UP:
44 | self.zoom += 1
45 | if self.zoom > 6:
46 | self.zoom = 6
47 | $Camera.size = zoom_list[self.zoom-1]
48 | _set_view()
49 | elif event.button_index == BUTTON_RIGHT:
50 | if len(hold_r) == 0:
51 | hold_r = [event.position.x, event.position.y]
52 | elif event.button_index == BUTTON_WHEEL_DOWN:
53 | self.zoom -= 1
54 | if self.zoom < 1:
55 | self.zoom = 1
56 | $Camera.size = zoom_list[self.zoom-1]
57 | _set_view()
58 | if not event.is_pressed():
59 | if event.button_index == BUTTON_RIGHT:
60 | hold_r = []
61 | elif event is InputEventKey:
62 | if event.pressed and event.scancode == KEY_PAGEUP:
63 | rotated = (rotated + 1)%4
64 | var rot = round(((rotated*(PI/2)) + get_node("../Spatial").rotation.y) / (PI/2))*(PI/2)
65 | var rot_trans = get_node("../Spatial").transform.rotated(Vector3(0,1,0), rot)
66 | get_node("../Spatial").set_transform(rot_trans)
67 | var old = self.transform.origin
68 | self.transform.origin = Vector3(-old.z, old.y, old.x)
69 | elif event.pressed and event.scancode == KEY_PAGEDOWN:
70 | rotated = ((rotated-1)+4)%4
71 | var rot = round(((rotated*(PI/2)) + get_node("../Spatial").rotation.y) / (PI/2))*(PI/2)
72 | var rot_trans = get_node("../Spatial").transform.rotated(Vector3(0,1,0), rot)
73 | get_node("../Spatial").set_transform(rot_trans)
74 | var old = self.transform.origin
75 | self.transform.origin = Vector3(old.z, old.y, -old.x)
76 | func _physics_process(_delta):
77 | self.move_and_slide(self.velocity)
78 |
79 | func _set_view():
80 | # Elevation angle, Azimuth is a class var since it doesn't change
81 | var El = deg2rad(elevations[zoom-1])
82 |
83 | # exposure angles D and E
84 | var D = atan( (cos(El)) / (tan(AZIMUTH)) )
85 | var E = atan( (cos(El)) * (tan(AZIMUTH)) )
86 | # axis shrink factors
87 | var Sx = sin(AZIMUTH) / cos(D)
88 | var Sz = cos(AZIMUTH) / cos(E)
89 | var Sy = sin(El)
90 | # range divisor, not sure why its needed
91 | var rng_d = 1
92 | # setting up the transform
93 | var v_trans = transform
94 | v_trans.basis.x = Vector3((Sx*cos(D))/rng_d, 0.0, Sx*sin(D)/rng_d)
95 | v_trans.basis.y = Vector3(0.0, 1.0, Sy/rng_d)
96 | v_trans.basis.z = Vector3(-(Sz*cos(E))/rng_d, -1.0, (Sz*sin(E))/rng_d)
97 | v_trans = v_trans.inverse()
98 | v_trans.origin = self.transform.origin
99 | self.transform = v_trans
100 | #print("in", v_trans, "\ncm", self.get_node("Camera").transform)
101 |
102 | # set sun location
103 | self.get_node("../Sun").transform.origin = Vector3(-50, 30, 20)
104 | self.get_node("../Sun").look_at(Vector3(0.0, 0.0, 0.0), Vector3(0.0, 1.0, 0.0))
105 |
106 | # set zoom related uniforms for shaders
107 | var mat = self.get_parent().get_node("Spatial/Terrain").get_material_override()
108 | mat.set_shader_param("zoom", zoom)
109 | mat.set_shader_param("tiling_factor", zoom)
110 | self.get_parent().get_node("Spatial/Terrain").set_material_override(mat)
111 | var mat_e = self.get_parent().get_node("Spatial/Border").get_material_override()
112 | mat_e.set_shader_param("zoom", zoom)
113 | mat_e.set_shader_param("tiling_factor", zoom)
114 | self.get_parent().get_node("Spatial/Border").set_material_override(mat_e)
115 | var mat_w = self.get_parent().get_node("Spatial/WaterPlane").get_material_override()
116 | mat_w.set_shader_param("zoom", zoom)
117 | mat_w.set_shader_param("tiling_factor", zoom)
118 | self.get_parent().get_node("Spatial/WaterPlane").set_material_override(mat_w)
119 |
120 |
--------------------------------------------------------------------------------
/CityView/CityScene/City.gd:
--------------------------------------------------------------------------------
1 | extends Spatial
2 |
3 | var size_w : int = 4
4 | var size_h : int = 4
5 | var rng : RandomNumberGenerator = RandomNumberGenerator.new()
6 | var savefile
7 | var width
8 | var height
9 | const TILE_SIZE : int = 16
10 | const WATER_HEIGHT : float = 250.0 / TILE_SIZE
11 | # stores where tiles are in the vertex, uv and normal arrays
12 | var terr_tile_ind = {}
13 | # translator from fsh-iid to texturearray layer
14 | var ind_layer
15 | # for cursor
16 | var cur_img
17 | var vec_hot
18 |
19 | func _ready():
20 | rng.randomize()
21 | savefile = Boot.current_city
22 | create_terrain()
23 | set_cursor()
24 | pass
25 |
26 | func gen_random_terrain(width : int, height : int) -> Array:
27 | var heightmap : Array = []
28 | var noise = OpenSimplexNoise.new()
29 | noise.seed = randi()
30 | noise.octaves = 1
31 | noise.period = 20
32 | noise.persistence = 0.8
33 | for i in range(width):
34 | heightmap.append([])
35 | for j in range(height):
36 | var h = WATER_HEIGHT * TILE_SIZE + (noise.get_noise_2d(i, j) * 200 )
37 | heightmap[i].append(h)
38 | return heightmap
39 |
40 | func load_city_terrain(svfile : DBPF):
41 | var heightmap : Array = []
42 | var city_info = svfile.get_subfile(0xca027edb, 0xca027ee1, 0, SC4ReadRegionalCity)
43 | var terrain_info = svfile.get_subfile(0xa9dd6ff4, 0xe98f9525, 00000001, cSTETerrain__SaveAltitudes)
44 | size_w = city_info.size[0]
45 | size_h = city_info.size[1]
46 | self.width = size_w * 64 + 1
47 | self.height = size_h * 64 + 1
48 |
49 | terrain_info.set_dimensions(self.width, self.height)
50 | for i in range(width):
51 | heightmap.append([])
52 | for j in range(self.height):
53 | heightmap[i].append(terrain_info.get_altitude(i, j))
54 | self.get_node("Spatial/Terrain").heightmap = heightmap
55 | return heightmap
56 |
57 | #(0, 0) (1, 0)
58 | # 0 - 3
59 | # | \ |
60 | # 1 - 2
61 | #(0, 1) (1, 1)
62 | func create_face(v0 : Vector3, v1 : Vector3, v2 : Vector3, v3 : Vector3, heightmap):
63 | """
64 | TODO:
65 | swap from surface normals to verex normals to make godot smoothen out edges
66 |
67 | """
68 | var v : Vector3 = v1 - v0
69 | var u1 : Vector3 = v2 - v0
70 | var u2 : Vector3 = v3 - v1
71 | var w1 : Vector3 = v3 - v0
72 | var w2 : Vector3 = v3 - v2
73 | var normal1 : Vector3 = v.cross(u1).normalized()
74 | var normal2 : Vector3 = u1.cross(w1).normalized()
75 | var normal3 : Vector3 = v.cross(u2).normalized()
76 | var normal4 : Vector3 = u2.cross(w2).normalized()
77 | var uvs = []
78 | var normalz = []
79 | var layer_ind = []
80 | var layer_weights = []
81 | var vertex_layers = []
82 | var vertex_weights = []
83 | for i in range(4):
84 | var vert = [v0, v1, v2, v3][i]
85 | var res = coord_to_uv(vert.x, vert.y, vert.z)
86 | uvs.append(res[0])
87 | if heightmap:
88 | var weight = [0.0, 0.0, 0.0, 0.0]
89 | weight[i] = 1.0
90 | vertex_layers.append(res[1])
91 | vertex_weights.append([weight[0], weight[1], weight[2], weight[3]])
92 | normalz.append(get_normal(vert, heightmap))
93 | else:
94 | normalz.append(Vector3(0.0, 1.0, 0.0))
95 |
96 | var vertices = PoolVector3Array()
97 | var normals = PoolVector3Array()
98 | var UVs = PoolVector2Array()
99 |
100 | # this if-else is my attempt to make the terrain diagonals smart, sometimes they still don't cooperate
101 | if min(normal1.y, normal2.y) >= min(normal3.y, normal4.y):
102 | var uv2
103 | var cols = [null, null, null]
104 | if heightmap:
105 | uv2 = Vector2(vertex_layers[2]/255.0, vertex_layers[1]/255.0)
106 | cols = [
107 | Color(vertex_weights[0][0], vertex_weights[0][2], vertex_weights[0][1], vertex_layers[0]/255.0),
108 | Color(vertex_weights[2][0], vertex_weights[2][2], vertex_weights[2][1], vertex_layers[0]/255.0),
109 | Color(vertex_weights[1][0], vertex_weights[1][2], vertex_weights[1][1], vertex_layers[0]/255.0),
110 | ]
111 | vertices.append(v0)
112 | UVs.append(uvs[0])
113 | normals.append(normalz[0])
114 | layer_ind.append(uv2)
115 | layer_weights.append(cols[0])
116 | vertices.append(v2)
117 | UVs.append(uvs[2])
118 | normals.append(normalz[2])
119 | layer_ind.append(uv2)
120 | layer_weights.append(cols[2])
121 | vertices.append(v1)
122 | UVs.append(uvs[1])
123 | normals.append(normalz[1])
124 | layer_ind.append(uv2)
125 | layer_weights.append(cols[1])
126 |
127 | if heightmap:
128 | uv2 = Vector2(vertex_layers[3]/255.0, vertex_layers[2]/255.0)
129 | cols = [
130 | Color(vertex_weights[0][0], vertex_weights[0][3], vertex_weights[0][2], vertex_layers[0]/255.0),
131 | Color(vertex_weights[3][0], vertex_weights[3][3], vertex_weights[3][2], vertex_layers[0]/255.0),
132 | Color(vertex_weights[2][0], vertex_weights[2][3], vertex_weights[2][2], vertex_layers[0]/255.0),
133 | ]
134 | vertices.append(v0)
135 | UVs.append(uvs[0])
136 | normals.append(normalz[0])
137 | layer_ind.append(uv2)
138 | layer_weights.append(cols[0])
139 | vertices.append(v3)
140 | UVs.append(uvs[3])
141 | normals.append(normalz[3])
142 | layer_ind.append(uv2)
143 | layer_weights.append(cols[1])
144 | vertices.append(v2)
145 | UVs.append(uvs[2])
146 | normals.append(normalz[2])
147 | layer_ind.append(uv2)
148 | layer_weights.append(cols[2])
149 |
150 | else:
151 | var uv2
152 | var cols = [null, null, null]
153 | if heightmap:
154 | uv2 = Vector2(vertex_layers[3]/255.0, vertex_layers[1]/255.0)
155 | cols = [
156 | Color(vertex_weights[0][0], vertex_weights[0][3], vertex_weights[0][1], vertex_layers[0]/255.0),
157 | Color(vertex_weights[3][0], vertex_weights[3][3], vertex_weights[3][1], vertex_layers[0]/255.0),
158 | Color(vertex_weights[1][0], vertex_weights[1][3], vertex_weights[1][1], vertex_layers[0]/255.0),
159 | ]
160 | vertices.append(v0)
161 | UVs.append(uvs[0])
162 | normals.append(normalz[0])
163 | layer_ind.append(uv2)
164 | layer_weights.append(cols[0])
165 | vertices.append(v3)
166 | UVs.append(uvs[3])
167 | normals.append(normalz[3])
168 | layer_ind.append(uv2)
169 | layer_weights.append(cols[1])
170 | vertices.append(v1)
171 | UVs.append(uvs[1])
172 | normals.append(normalz[1])
173 | layer_ind.append(uv2)
174 | layer_weights.append(cols[2])
175 |
176 | if heightmap:
177 | uv2 = Vector2(vertex_layers[2]/255.0, vertex_layers[1]/255.0)
178 | cols = [
179 | Color(vertex_weights[3][3], vertex_weights[3][2], vertex_weights[3][1], vertex_layers[3]/255.0),
180 | Color(vertex_weights[2][3], vertex_weights[2][2], vertex_weights[2][1], vertex_layers[3]/255.0),
181 | Color(vertex_weights[1][3], vertex_weights[1][2], vertex_weights[1][1], vertex_layers[3]/255.0),
182 | ]
183 | vertices.append(v3)
184 | UVs.append(uvs[3])
185 | normals.append(normalz[3])
186 | layer_ind.append(uv2)
187 | layer_weights.append(cols[0])
188 | vertices.append(v2)
189 | UVs.append(uvs[2])
190 | normals.append(normalz[2])
191 | layer_ind.append(uv2)
192 | layer_weights.append(cols[1])
193 | vertices.append(v1)
194 | UVs.append(uvs[1])
195 | normals.append(normalz[1])
196 | layer_ind.append(uv2)
197 | layer_weights.append(cols[2])
198 |
199 | return [vertices, normals, UVs, layer_ind, layer_weights]
200 |
201 | func create_edge(vert, n1, n2, normal):
202 | """
203 | in: 4 vertices in array and two normals
204 | out: appropriate triangle vertices for 3 levels of edge depth
205 | normals influence the thickness of the top two layers
206 |
207 | could i just do long quads and handle the rest in the fragment shader?
208 | how would the deterministic randomness be achieved? adding different phase and altitude sines?
209 | how would a fragment know its distance from the surface?
210 | can pass a Varying smooth y and a Varying static y from vertex to fragment,
211 | the difference will then indicate when to switch texture
212 | uv's will then just be in direct coordination to the height
213 |
214 | Static Noise texture (can also be used to spice up terrain randomness only needs to be set once)
215 | Long Quads -> Varying smooth and static -> uv.y's represent height,
216 | should jiggle bottom uv.y's based on normal.y's
217 | """
218 | var TerrainTexTilingFactor = 0.2
219 | var factor = (16.0/100.0) * TerrainTexTilingFactor
220 | var coords = [vert[0].x, vert[1].x, vert[2].x, vert[3].x]
221 | if abs(normal.x) < abs(normal.z):
222 | coords = [vert[0].z, vert[1].z, vert[2].z, vert[3].z]
223 | var uv0 = Vector2(float(coords[0]) * factor, 0.0)
224 | var uv1 = Vector2(float(coords[1]) * factor, 0.0)
225 | var uv2 = Vector2(float(coords[2]) * factor, vert[1].y * factor)
226 | var uv3 = Vector2(float(coords[3]) * factor, vert[0].y * factor)
227 | var vertices = []
228 | var normals = []
229 | var UVs = []
230 | vertices.append(vert[2])
231 | UVs.append(uv2)
232 | normals.append(normal)
233 | vertices.append(vert[1])
234 | UVs.append(uv1)
235 | normals.append(normal)
236 | vertices.append(vert[0])
237 | UVs.append(uv0)
238 | normals.append(normal)
239 | vertices.append(vert[3])
240 | UVs.append(uv3)
241 | normals.append(normal)
242 | vertices.append(vert[2])
243 | UVs.append(uv2)
244 | normals.append(normal)
245 | vertices.append(vert[0])
246 | UVs.append(uv0)
247 | normals.append(normal)
248 | return [vertices, normals, UVs]
249 |
250 | func create_terrain():
251 | self.ind_layer = $Spatial/Terrain.load_textures_to_uv_dict()
252 | var vertices : PoolVector3Array = PoolVector3Array()
253 | var normals : PoolVector3Array = PoolVector3Array()
254 | var UVs : PoolVector2Array = PoolVector2Array()
255 | var col_layer_weights : PoolColorArray = PoolColorArray()
256 | var uv2_layer_ind : PoolVector2Array = PoolVector2Array()
257 | var e_vertices : PoolVector3Array = PoolVector3Array()
258 | var e_normals : PoolVector3Array = PoolVector3Array()
259 | var e_UVs : PoolVector2Array = PoolVector2Array()
260 | var w_vertices : PoolVector3Array = PoolVector3Array()
261 | var w_normals : PoolVector3Array = PoolVector3Array()
262 | var w_UVs : PoolVector2Array = PoolVector2Array()
263 | # Random heightmap (for now)
264 | var heightmap : Array
265 | if savefile != null:
266 | heightmap = load_city_terrain(savefile)
267 | else:
268 | heightmap = gen_random_terrain(size_w * 64 + 1, size_h * 64 + 1)
269 | $Spatial/WaterPlane.generate_wateredges(heightmap)
270 | var tiles_w = size_w * 64
271 | var tiles_h = size_h * 64
272 |
273 | var v1
274 | var v2
275 | var v3
276 | var v4
277 | var ve1
278 | var ve2
279 | var ve3
280 | var ve4
281 | var vw1
282 | var vw2
283 | var vw3
284 | var vw4
285 | var n_in1
286 | var n_in2
287 | # Top surface
288 | for i in range(tiles_w):
289 | for j in range(tiles_h):
290 | v1 = Vector3(i, heightmap[j ][i ] / TILE_SIZE, j )
291 | v2 = Vector3(i, heightmap[j+1][i ] / TILE_SIZE, (j+1))
292 | v3 = Vector3((i+1), heightmap[j+1][i+1] / TILE_SIZE, (j+1))
293 | v4 = Vector3((i+1), heightmap[j ][i+1] / TILE_SIZE, j )
294 | var r = create_face(v1, v2, v3, v4, heightmap)
295 | terr_tile_ind[Vector2(v1.x, v1.z)] = len(vertices)
296 | vertices.append_array(r[0])
297 | normals.append_array(r[1])
298 | UVs.append_array(r[2])
299 | uv2_layer_ind.append_array(r[3])
300 | col_layer_weights.append_array(r[4])
301 | if i == 0:
302 | ve1 = v2
303 | ve2 = v1
304 | ve3 = Vector3(ve1.x, 0.0, ve2.z)
305 | ve4 = Vector3(ve2.x, 0.0, ve1.z)
306 | n_in1 = r[1][0]
307 | n_in2 = r[1][1]
308 | var e = create_edge([ve1, ve2, ve3, ve4], n_in1, n_in2, Vector3(0.0, 0.0, -1.0))
309 | e_vertices.append_array(e[0])
310 | e_normals.append_array(e[1])
311 | e_UVs.append_array(e[2])
312 | if i == tiles_w - 1:
313 | ve1 = v4
314 | ve2 = v3
315 | ve3 = Vector3(ve2.x, 0.0, ve2.z)
316 | ve4 = Vector3(ve1.x, 0.0, ve1.z)
317 | n_in1 = r[1][3]
318 | n_in2 = r[1][2]
319 | var e = create_edge([ve1, ve2, ve3, ve4], n_in1, n_in2, Vector3(0.0, 0.0, 1.0))
320 | e_vertices.append_array(e[0])
321 | e_normals.append_array(e[1])
322 | e_UVs.append_array(e[2])
323 | if j == 0:
324 | ve1 = v1
325 | ve2 = v4
326 | ve3 = Vector3(ve2.x, 0.0, ve2.z)
327 | ve4 = Vector3(ve1.x, 0.0, ve1.z)
328 | n_in1 = r[1][0]
329 | n_in2 = r[1][3]
330 | var e = create_edge([ve1, ve2, ve3, ve4], n_in1, n_in2, Vector3(1.0, 0.0, 0.0))
331 | e_vertices.append_array(e[0])
332 | e_normals.append_array(e[1])
333 | e_UVs.append_array(e[2])
334 | if j == tiles_h - 1:
335 | ve1 = v3
336 | ve2 = v2
337 | ve3 = Vector3(ve2.x, 0.0, ve2.z)
338 | ve4 = Vector3(ve1.x, 0.0, ve1.z)
339 | n_in1 = r[1][2]
340 | n_in2 = r[1][1]
341 | var e = create_edge([ve1, ve2, ve3, ve4], n_in1, n_in2, Vector3(-1.0, 0.0, 0.0))
342 | e_vertices.append_array(e[0])
343 | e_normals.append_array(e[1])
344 | e_UVs.append_array(e[2])
345 |
346 | vw1 = Vector3(i, WATER_HEIGHT, j)
347 | vw2 = Vector3(i, WATER_HEIGHT, j+1)
348 | vw3 = Vector3(i+1, WATER_HEIGHT, j+1)
349 | vw4 = Vector3(i+1, WATER_HEIGHT, j)
350 | var wa = create_face(vw1, vw2, vw3, vw4, null)
351 | w_vertices.append_array(wa[0])
352 | w_normals.append_array(wa[1])
353 | w_UVs.append_array(wa[2])
354 |
355 | var array_mesh : ArrayMesh = ArrayMesh.new()
356 | var arrays : Array = []
357 | arrays.resize(ArrayMesh.ARRAY_MAX)
358 | arrays[ArrayMesh.ARRAY_VERTEX] = vertices
359 | arrays[ArrayMesh.ARRAY_NORMAL] = normals
360 | arrays[ArrayMesh.ARRAY_TEX_UV] = UVs
361 | arrays[ArrayMesh.ARRAY_TEX_UV2] = uv2_layer_ind
362 | arrays[ArrayMesh.ARRAY_COLOR] = col_layer_weights
363 | array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
364 | $Spatial/Terrain.mesh = array_mesh
365 | $Spatial/Terrain.create_trimesh_collision()
366 |
367 | #var layer_img = Image.new()
368 | #var layer_flat = PoolByteArray([])
369 | #for row in self.layer_arr:
370 | # layer_flat.append_array(row)
371 | #layer_img.create_from_data(self.width, self.height, false, Image.FORMAT_R8, layer_flat)
372 | #var layer_tex = ImageTexture.new()
373 | #layer_tex.create_from_image(layer_img, 2)
374 | #var mat = $Spatial/Terrain.get_material_override()
375 | #mat.set_shader_param("layer", layer_tex)
376 | #$Spatial/Terrain.set_material_override(mat)
377 |
378 | var e_rray_mesh : ArrayMesh = ArrayMesh.new()
379 | var e_rrays : Array = []
380 | e_rrays.resize(ArrayMesh.ARRAY_MAX)
381 | e_rrays[ArrayMesh.ARRAY_VERTEX] = e_vertices
382 | e_rrays[ArrayMesh.ARRAY_NORMAL] = e_normals
383 | e_rrays[ArrayMesh.ARRAY_TEX_UV] = e_UVs
384 | e_rray_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, e_rrays)
385 | $Spatial/Border.mesh = e_rray_mesh
386 |
387 | var warray_mesh : ArrayMesh = ArrayMesh.new()
388 | var warrays : Array = []
389 | warrays.resize(ArrayMesh.ARRAY_MAX)
390 | warrays[ArrayMesh.ARRAY_VERTEX] = w_vertices
391 | warrays[ArrayMesh.ARRAY_NORMAL] = w_normals
392 | warrays[ArrayMesh.ARRAY_TEX_UV] = w_UVs
393 | warray_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, warrays)
394 | $Spatial/WaterPlane.mesh = warray_mesh
395 |
396 | "test s3d"
397 | var TGI_s3d = {"T": 0x5ad0e817, "G": 0xbadb57f1, "I":0x16620430}
398 | var s3dobj = Core.subfile(TGI_s3d["T"], TGI_s3d["G"], TGI_s3d["I"], S3DSubfile)
399 | var location = Vector3(width/2, heightmap[int(width/2)][int(self.height/2)] / TILE_SIZE, self.height/2)
400 | for x in range(5):
401 | for z in range(5):
402 | var x_rand = (x-2.5) + randf()
403 | var z_rand = (z-2.5) + randf()
404 | var loc_rand = location + Vector3(x_rand, 0, z_rand)
405 | s3dobj.add_to_mesh($Spatial/TestS3D, loc_rand)
406 | test_exemplar()
407 | var s3dmat = $Spatial/TestS3D.get_material_override()
408 | s3dmat.set_shader_param("nois_texture", $Spatial/WaterPlane/NoiseTexture.texture)
409 | $Spatial/TestS3D.set_material_override(s3dmat)
410 | print("DEBUG")
411 |
412 | func set_cursor():
413 | var TGI_cur = {"T": 0xaa5c3144, "G": 0x00000032, "I":0x13b138d0}
414 | self.vec_hot = Core.subfile(TGI_cur["T"], TGI_cur["G"], TGI_cur["I"], CURSubfile).entries[0].vec_hotspot
415 | self.cur_img = Core.subfile(TGI_cur["T"], TGI_cur["G"], TGI_cur["I"], CURSubfile).get_as_texture()
416 | Input.set_custom_mouse_cursor(cur_img, Input.CURSOR_ARROW, vec_hot)
417 |
418 | func coord_to_uv(x, y, z):
419 | var TerrainTexTilingFactor = 0.2 # 0x6534284a,0x88cd66e9,0x00000001 describes this as 100m of terrain corresponds to this fraction of texture in farthest zoom
420 | var x_factored = (float(x)*16.0/100.0) * TerrainTexTilingFactor
421 | var y_factored = (float(z)*16.0/100.0) * TerrainTexTilingFactor
422 | var temp = max(min(32-int((y-15.0) * 1.312), 31),0) # 0x6534284a,0x7a4a8458,0x1a2fdb6b describes AltitudeTemperatureFactor of 0.082, i multiplied this by 16
423 |
424 | var moist = 6
425 | var inst_key = $Spatial/Terrain.tm_table[temp][moist]
426 | return [Vector2(x_factored, y_factored), self.ind_layer[inst_key]]
427 |
428 | func get_normal(vert : Vector3, heightmap):
429 | var min_x = 0.0
430 | if vert.x > 0:
431 | min_x = -1.0
432 | var max_x = 0.0
433 | if vert.x < (len(heightmap)-1):
434 | max_x = 1.0
435 | var min_z = 0.0
436 | if vert.z > 0:
437 | min_z = -1.0
438 | var max_z = 0.0
439 | if vert.z < (len(heightmap)-1):
440 | max_z = 1.0
441 | var vert_c = [[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]]
442 | var vertices = []
443 | for coord in vert_c:
444 | vertices.append(
445 | Vector3(vert.x + coord[0],
446 | (heightmap[(vert.z) + min(max(coord[1], min_z), max_z)][(vert.x) + min(max(coord[0], min_x), max_x)])/16.0,
447 | vert.z + coord[1])
448 | )
449 | var s_normals = Vector3(0.0, 0.0, 0.0)
450 | #print(vert, "\t", vertices)
451 | for v_i in range(len(vertices)):
452 | var v1 = vert
453 | var v2 = vertices[v_i]
454 | var v3 = vertices[(v_i - 1)%(len(vertices)-1)]
455 | var v : Vector3 = v2 - v1
456 | var u : Vector3 = v3 - v1
457 | var normal : Vector3 = v.cross(u).normalized()
458 | #var normal2 : Vector3 = u.cross(v)
459 | #print([v1, v2, v3], "\t", u, "\t", v, "\t", normal, "\t", normal2)
460 | s_normals = s_normals + normal
461 | var norm = (s_normals/len(vertices)).normalized()
462 | return norm
463 |
464 | func test_exemplar():
465 | var TGI = {"T": 0x6534284a, "G": 0xea12f32c, "I": 0x5}
466 | var exemplar = Core.subfile(TGI["T"], TGI["G"], TGI["I"], ExemplarSubfile)
467 | print("ParentCohort", exemplar.parent_cohort)
468 | for key in exemplar.properties.keys():
469 | print(key, "\t", exemplar.key_description(key), "\t", exemplar.properties[key])
470 |
471 |
--------------------------------------------------------------------------------
/CityView/CityScene/City.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=23 format=2]
2 |
3 | [ext_resource path="res://CityView/CityScene/City.gd" type="Script" id=1]
4 | [ext_resource path="res://CityView/CityScene/CameraAnchor3D.gd" type="Script" id=2]
5 | [ext_resource path="res://CityView/Meshes/Terrain.gd" type="Script" id=3]
6 | [ext_resource path="res://CityView/Meshes/WaterPlane.gd" type="Script" id=4]
7 | [ext_resource path="res://CityView/Meshes/TestS3D.gd" type="Script" id=5]
8 | [ext_resource path="res://CityView/Meshes/TransitTiles.gd" type="Script" id=6]
9 |
10 | [sub_resource type="ProceduralSky" id=15]
11 | radiance_size = 0
12 | sky_top_color = Color( 0.909804, 0.92549, 0.933333, 1 )
13 | sky_horizon_color = Color( 1, 1, 1, 1 )
14 | ground_bottom_color = Color( 0.909804, 0.92549, 0.933333, 1 )
15 | ground_horizon_color = Color( 1, 1, 1, 1 )
16 | texture_size = 0
17 |
18 | [sub_resource type="Environment" id=1]
19 | background_sky = SubResource( 15 )
20 | background_sky_custom_fov = 92.2
21 | background_color = Color( 0.317647, 0.341176, 0.352941, 1 )
22 | background_energy = 0.0
23 | ambient_light_color = Color( 1, 1, 1, 1 )
24 | ambient_light_energy = 0.3
25 |
26 | [sub_resource type="Shader" id=9]
27 | code = "shader_type spatial;
28 | render_mode depth_draw_always;
29 |
30 | uniform sampler2D watermap;
31 | // 512 to be used as 16 pixels per tile(1 per m) with octaves using 25 and 39.0625
32 | // first octave means the texture encompasses 32 tiles before repeating, 10.24, 6.55 tiles for octaves
33 | // to reduce noticable repeatedness octaves should not repeat at the same interger number of tiles
34 | uniform sampler2D noise_texture; //512x512 pixel noise texture
35 | uniform sampler2D noise_normals; //provides prebaked normals
36 | uniform int noise_octaves = 3; //number of scale steps used for layering
37 | uniform float noise_persistance = 0.64; //strength of octaves; 1.0 -> 0.4 -> 0.16
38 | uniform float noise_lacunarity = 0.94; //scale step between ocraves; 1.0 -> 0.6 -> 0.36
39 | uniform float noise_angle_ransomness = 8.0; //range for randomness of direction
40 | uniform vec2 noise_direction = vec2(0.4, 0.2); //controlls direction of motion
41 | uniform float noise_time_scale = 60.0; //speed control
42 | uniform float wave_height_scale = 4.0;
43 | uniform float wave_width_scale = 60.0;
44 | uniform float normal_horizontal = 1.0;
45 | uniform float normal_vertical = 1.0;
46 | uniform float beach_wave_str = 10.0;
47 | uniform float beach_wave_threshold = 0.9;
48 | uniform float open_wave_str = 20.0;
49 | uniform float open_wave_threshold = 0.95;
50 | varying float noise_height;
51 |
52 | uniform sampler2DArray watertexture : hint_albedo;
53 | uniform int zoom;
54 | uniform float tiling_factor;
55 | uniform float max_depth;
56 | uniform float depth_range;
57 | uniform float water_fog_density: hint_range(0.0, 2.0) = 1.668;
58 | varying vec2 coord;
59 | varying highp vec4 n_val1;
60 | varying highp vec4 n_val2;
61 | varying highp vec4 n_val3;
62 |
63 | uniform float rim = 1.0;
64 | uniform float metallic = 1.0;
65 | uniform float roughness = 0.3;
66 |
67 | vec4 noise_values(vec2 coords, int oct){
68 | float pers_str = pow(noise_persistance, float(oct));
69 | float lacu_scl = pow(noise_lacunarity, float(oct));
70 | vec2 rand_vec = vec2((2.0*texelFetch(noise_texture, ivec2(53*oct, 47*oct), 0).x -1.0) * noise_angle_ransomness, (2.0*texelFetch(noise_texture, ivec2(47*oct, 53*oct), 0).x -1.0) * noise_angle_ransomness);
71 | vec2 flow = ((noise_direction + rand_vec) * (TIME * noise_time_scale))*(lacu_scl+4.0);
72 | vec2 pixel_loc = ((coords+flow) * wave_width_scale * (1.0/lacu_scl));//, vec2(2048.0, 2048.0));
73 |
74 | vec4 values = vec4(pixel_loc, pers_str, 1.0);
75 | return values;
76 | }
77 |
78 | void vertex(){
79 | coord = VERTEX.xz;
80 | noise_height = 0.0;
81 | n_val1 = noise_values(coord, 0);
82 | noise_height += (pow(texture(noise_texture, mod(n_val1.rg/1024.0, 1.0)).x, 2.0) * n_val1.b);
83 | n_val2 = noise_values(coord, 1);
84 | noise_height -= (pow(texture(noise_texture, mod(n_val2.gr/1024.0, 1.0)).x, 2.0) * n_val2.b);
85 | n_val3 = noise_values(coord, 2);
86 | noise_height -= (pow(texture(noise_texture, mod(n_val3.rg/1024.0, 1.0)).x, 2.0) * n_val3.b);
87 | noise_height = 2.0 * (noise_height / (n_val1.b + n_val2.b + n_val3.b)) - 1.0;
88 | VERTEX.y += noise_height*wave_height_scale;
89 |
90 | }
91 |
92 | void fragment(){
93 |
94 | float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r;
95 | vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
96 | vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
97 | view.xyz /= view.w;
98 |
99 | float delta_depth = abs((view.z) - (VERTEX.z));
100 | float depth_alpha = 1.0;
101 | if (delta_depth < 6.0){
102 | depth_alpha = min((delta_depth / 5.0) + 0.1, 1.0);
103 | }
104 | float zoom_ind = min( float(zoom-1), 4.0);
105 | vec3 noisenormal = vec3(0.0);
106 | vec4 noise_tex = texture(noise_normals, mod(n_val1.rg/1024.0, 1.0));// * vec4(2.0, 2.0, 1.0, 1.0) - vec4(1.0, 1.0, 0.0, 0.0);
107 | noisenormal += (normalize(vec3(normal_horizontal*(2.0*sqrt(noise_tex.r)-1.0), normal_vertical*(2.0*sqrt(noise_tex.b)-1.0), normal_horizontal*(2.0*sqrt(noise_tex.g)-1.0))) * n_val1.b);
108 | noise_tex = texture(noise_normals, mod(n_val2.rg/1024.0, 1.0));// * vec4(2.0, 2.0, 1.0, 1.0) - vec4(1.0, 1.0, 0.0, 0.0);
109 | noisenormal += (normalize(vec3(normal_horizontal*(2.0*(noise_tex.g)-1.0), normal_vertical*(2.0*(noise_tex.b)-1.0), normal_horizontal*(2.0*(noise_tex.r)-1.0))) * n_val2.b);
110 | noise_tex = texture(noise_normals, mod(n_val3.rg/1024.0, 1.0));// * vec4(2.0, 2.0, 1.0, 1.0) - vec4(1.0, 1.0, 0.0, 0.0);
111 | noisenormal -= (normalize(vec3(normal_horizontal*(2.0*sqrt(noise_tex.r)-1.0), normal_vertical*(2.0*sqrt(noise_tex.b)-1.0), normal_horizontal*(2.0*sqrt(noise_tex.g)-1.0))) * n_val3.b);
112 | NORMAL = normalize((PROJECTION_MATRIX * INV_CAMERA_MATRIX * WORLD_MATRIX * vec4(normalize(noisenormal), 0.0)).rgb);
113 | vec3 break_col = vec3(0.0);
114 | if (noisenormal.y/3.0 < open_wave_threshold){
115 | break_col = vec3((open_wave_threshold - noisenormal.y/3.0)*open_wave_str);
116 | }
117 | if (depth_alpha < 0.8){
118 | if (noisenormal.y/3.0 < beach_wave_threshold){
119 | break_col += min(((depth_alpha-0.1)*3.0), 1.0) * pow(vec3(1.0 - (1.25*depth_alpha)) * vec3((beach_wave_threshold - noisenormal.y/3.0)*beach_wave_str), vec3(2.0));
120 | depth_alpha += break_col.r;
121 | }
122 | }
123 |
124 | RIM = rim;
125 | METALLIC = metallic;
126 | ROUGHNESS = roughness;
127 | ALBEDO = texture(watertexture, vec3(vec2(0.0, 0.0), float(zoom_ind))).rgb + break_col;
128 | ALPHA = sqrt(depth_alpha);
129 | //ALBEDO = vec3(normalize(noisenormal))*.5+.5;
130 | }"
131 |
132 | [sub_resource type="ShaderMaterial" id=10]
133 | shader = SubResource( 9 )
134 | shader_param/noise_octaves = 3
135 | shader_param/noise_persistance = 0.77
136 | shader_param/noise_lacunarity = 0.615
137 | shader_param/noise_angle_ransomness = 10.0
138 | shader_param/noise_direction = Vector2( -0.5, -0.4 )
139 | shader_param/noise_time_scale = 0.007
140 | shader_param/wave_height_scale = 0.09
141 | shader_param/wave_width_scale = 100.0
142 | shader_param/normal_horizontal = 1.156
143 | shader_param/normal_vertical = 1.0
144 | shader_param/beach_wave_str = 70.0
145 | shader_param/beach_wave_threshold = 0.372
146 | shader_param/open_wave_str = 50.0
147 | shader_param/open_wave_threshold = 0.348
148 | shader_param/zoom = null
149 | shader_param/tiling_factor = null
150 | shader_param/max_depth = null
151 | shader_param/depth_range = null
152 | shader_param/water_fog_density = 1.668
153 | shader_param/rim = 0.0
154 | shader_param/metallic = 0.0
155 | shader_param/roughness = 0.0
156 |
157 | [sub_resource type="OpenSimplexNoise" id=11]
158 | seed = 23
159 | octaves = 1
160 | persistence = 0.64
161 | lacunarity = 0.64
162 |
163 | [sub_resource type="NoiseTexture" id=12]
164 | flags = 0
165 | width = 1024
166 | height = 1024
167 | seamless = true
168 | noise = SubResource( 11 )
169 |
170 | [sub_resource type="OpenSimplexNoise" id=13]
171 | seed = 23
172 | octaves = 1
173 | period = 32.0
174 | persistence = 0.64
175 | lacunarity = 0.64
176 |
177 | [sub_resource type="NoiseTexture" id=14]
178 | flags = 22
179 | width = 1024
180 | height = 1024
181 | seamless = true
182 | as_normalmap = true
183 | bump_strength = 10.0
184 | noise = SubResource( 13 )
185 |
186 | [sub_resource type="Shader" id=4]
187 | code = "shader_type spatial;
188 | render_mode depth_draw_always;
189 |
190 | uniform sampler2DArray terrain : hint_albedo;
191 | uniform int zoom;
192 | uniform sampler2D layer;
193 | uniform sampler2D watermap;
194 | varying smooth vec3 coord_pass;
195 | varying smooth float map_str;
196 | const float PI = 3.14159265358979323846;
197 | uniform float cliff_ind;
198 | uniform float beach_ind;
199 | uniform float tiling_factor;
200 | uniform float water_height = 15.625; //250.0/16.0
201 | uniform bool grid_bool = false;
202 | uniform float beach_ht_range;
203 | varying float norm_x;
204 | varying float norm_y;
205 | varying float norm_z;
206 | varying flat float layer_i_0;
207 | varying flat float layer_i_1;
208 | varying flat float layer_i_2;
209 | varying float beach_str;
210 | //varying flat vec3 normal;
211 |
212 | void vertex()
213 | {
214 | coord_pass = VERTEX.xyz;
215 |
216 | map_str = (texelFetch(watermap, ivec2(coord_pass.zx), 0).r*beach_ht_range);
217 | vec3 w_norm = normalize((vec4(NORMAL, 0.0)).rgb);
218 | norm_x = w_norm.x;
219 | norm_y = w_norm.y;
220 | norm_z = w_norm.z;
221 | layer_i_0 = COLOR.a * 255.0;
222 | layer_i_1 = UV2.x * 255.0;
223 | layer_i_2 = UV2.y * 255.0;
224 | beach_str = pow(min((texelFetch(watermap, ivec2(coord_pass.xz).yx, 0).r) * 100.0, 1.0), 4.0);
225 | //normal = normalize((inverse(WORLD_MATRIX) * vec4(NORMAL, 0.0)).rgb);
226 | //if (map_str < (beach_ht_range/2.0)){
227 | // map_str = min(map_str, 1.0);
228 | //}
229 | //else{
230 | // map_str = 0.0;
231 | //}
232 | }
233 |
234 | void fragment()
235 | {
236 | float zoom_ind = min( float(zoom-1), 4.0);
237 | vec2 coord = coord_pass.xz;
238 |
239 | float layer_w_0 = COLOR.r;
240 | float layer_w_1 = COLOR.g;
241 | float layer_w_2 = COLOR.b;
242 | // might not be the fastest way of doing this
243 | ivec2 icoord1 = ivec2(coord);
244 | ivec2 icoord2 = icoord1 + ivec2(0, 1);
245 | ivec2 icoord3 = icoord1 + ivec2(1, 0);
246 |
247 | vec3 beach_col = texture(terrain, vec3(UV.y*tiling_factor, UV.x*tiling_factor, float(zoom_ind)+beach_ind)).rgb;
248 |
249 | vec2 rem1 = abs(vec2(icoord1) - coord);
250 | vec2 rem2 = abs(vec2(icoord2) - coord);
251 | vec2 rem3 = abs(vec2(icoord3) - coord);
252 | vec3 grid = vec3(1.0, 1.0, 1.0);
253 | if (grid_bool){
254 | if (rem1.x < (0.05 * max((5.0-float(zoom_ind)), 0.0)) ){
255 | grid = vec3(0.6, 0.6, 0.6);
256 | }
257 | if (rem1.y < (0.05 * max((5.0-float(zoom_ind)), 0.0)) and grid_bool == true){
258 | grid = vec3(0.6, 0.6, 0.6);
259 | }
260 | }
261 | float sum_str = layer_w_0 + layer_w_1 + layer_w_2;
262 | vec3 col1 = (texture(terrain, vec3(UV.x*tiling_factor, UV.y*tiling_factor, float(zoom_ind)+layer_i_0)).rgb * (1.0 - beach_str) + (beach_str * beach_col)) * (layer_w_0/sum_str);
263 | vec3 col2 = (texture(terrain, vec3(UV.x*tiling_factor, UV.y*tiling_factor, float(zoom_ind)+layer_i_1)).rgb * (1.0 - beach_str) + (beach_str * beach_col)) * (layer_w_1/sum_str);
264 | vec3 col3 = (texture(terrain, vec3(UV.x*tiling_factor, UV.y*tiling_factor, float(zoom_ind)+layer_i_2)).rgb * (1.0 - beach_str) + (beach_str * beach_col)) * (layer_w_2/sum_str);
265 | //cliffs
266 | vec3 overlay = vec3(0.0, 0.0, 0.0);
267 | vec3 underlay = vec3(0.0, 0.0, 0.0);
268 | float over_str = 0.0;
269 | //vec3 normal = normalize((vec4(NORMAL, 0.0) * inverse(WORLD_MATRIX)).xyz);
270 | vec3 normal = normalize(vec3(norm_x, norm_y, norm_z));
271 | if (normal.y < 0.75){
272 | over_str = (max(min(((0.75-(normal.y))*8.0), 1.0), 0.01));
273 | if (abs(normal.x) > abs(normal.z)){
274 | overlay = texture(terrain, vec3(UV.y*2.0*tiling_factor, UV.x*2.0*tiling_factor, float(zoom_ind)+cliff_ind)).rgb;
275 | }
276 | else{
277 | overlay = texture(terrain, vec3(UV.x*2.0*tiling_factor, UV.y*2.0*tiling_factor, float(zoom_ind)+cliff_ind)).rgb;
278 | }
279 | }
280 | ALBEDO = (
281 | (
282 | (
283 | (col1 + col2 + col3) * (1.0-over_str)
284 | )
285 | + (overlay * over_str)
286 | )
287 | ) * grid;
288 | //ALBEDO = vec3(normal.g)*2.0-1.0;
289 |
290 | }"
291 |
292 | [sub_resource type="ShaderMaterial" id=5]
293 | shader = SubResource( 4 )
294 |
295 | [sub_resource type="Shader" id=16]
296 | code = "shader_type spatial;
297 | render_mode depth_draw_always;
298 |
299 | uniform sampler2DArray s3dtexture : hint_albedo;
300 | uniform sampler2D nois_texture: hint_albedo;
301 | uniform vec2 direction = vec2(1.0, 0.0);
302 |
303 | void fragment(){
304 | ivec2 iUV = (ivec2(UV * vec2(1280, 720) * vec2(341) + (direction * TIME))%ivec2(1024));
305 | vec3 noise_col = pow(texelFetch(nois_texture, iUV, 0).rgb * vec3(2.0), vec3(2.0));//*vec3(0.4, 0.8, 0.4);
306 | ALBEDO = min(texture(s3dtexture, vec3(UV, 0.0)).rgb * noise_col, vec3(1.0));
307 | ALPHA = texture(s3dtexture, vec3(UV, 0.0)).a;
308 | ALPHA_SCISSOR = 0.9;
309 | }
310 | "
311 |
312 | [sub_resource type="ShaderMaterial" id=17]
313 | shader = SubResource( 16 )
314 | shader_param/direction = Vector2( 40, 20 )
315 |
316 | [sub_resource type="Shader" id=7]
317 | code = "shader_type spatial;
318 | render_mode depth_draw_always;
319 |
320 | uniform sampler2DArray terrain : hint_albedo;
321 | uniform int zoom;
322 | varying smooth float height_var;
323 | varying flat float height_flat;
324 | const float PI = 3.14159265358979323846;
325 | uniform float tiling_factor;
326 | uniform float top_ind;
327 | uniform float mid_ind;
328 | uniform float bot_ind;
329 |
330 | void vertex()
331 | {
332 | height_var = VERTEX.y;
333 | height_flat = VERTEX.y;
334 |
335 | }
336 |
337 | void fragment()
338 | {
339 | float edge_tex = 0.0;
340 | float zoom_ind = min(float(zoom), 4.0); // only farthest zooms have mipmaps
341 | if (UV.y > 0.3152){
342 | edge_tex = bot_ind;
343 | }
344 | else{
345 | if(UV.y > 0.1576){
346 | edge_tex = mid_ind;
347 | }
348 | else{
349 | edge_tex = top_ind;
350 | }
351 | }
352 | float uvx = UV.x*tiling_factor;
353 | float uvy = UV.y*tiling_factor;
354 | if (zoom < 4){
355 | float div = 1.0 / pow(2.0, float(4-zoom)); // farther zooms (lower zoom value) have smaller textures
356 | uvx = mod(uvx, div);
357 | uvy = mod(uvy, div);
358 | }
359 | ALBEDO = texture(terrain, vec3(uvx, uvy, float(zoom_ind)+edge_tex)).rgb;
360 | }"
361 |
362 | [sub_resource type="ShaderMaterial" id=8]
363 | shader = SubResource( 7 )
364 | shader_param/zoom = null
365 | shader_param/tiling_factor = null
366 | shader_param/top_ind = null
367 | shader_param/mid_ind = null
368 | shader_param/bot_ind = null
369 |
370 | [sub_resource type="Shader" id=18]
371 | code = "shader_type spatial;
372 | render_mode depth_draw_always;
373 |
374 | uniform sampler2DArray textarr : hint_albedo;
375 | varying float built;
376 | //uniform sampler2D layer : hint_albedo;
377 | varying flat float index;
378 |
379 | void vertex(){
380 | built = 0.0;
381 | if (UV2.g > 127.0){
382 | built = 1.0;
383 | }
384 | index = (UV2.r) + (floor(mod(UV2.g, 128.0)) * 256.0);
385 |
386 | }
387 |
388 | void fragment(){
389 | vec4 frag = texture(textarr, vec3(UV, index));
390 | float alpha = frag.a;
391 | vec4 col = vec4(1.0);
392 | if(built == 0.0){
393 | col = COLOR;
394 | alpha = col.a;
395 | }
396 | ALBEDO = frag.rgb * col.rgb;
397 | ALPHA = alpha;
398 | }"
399 |
400 | [sub_resource type="ShaderMaterial" id=19]
401 | render_priority = 1
402 | shader = SubResource( 18 )
403 |
404 | [node name="City" type="Spatial"]
405 | script = ExtResource( 1 )
406 |
407 | [node name="Sun" type="DirectionalLight" parent="."]
408 | transform = Transform( 1, 0, 0, 0, 1, 7.81531e-08, 0, -7.81531e-08, 1, 0, 0, 0 )
409 | light_specular = 0.0
410 | light_bake_mode = 0
411 | shadow_enabled = true
412 | shadow_bias = 1.422
413 | directional_shadow_mode = 0
414 |
415 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
416 | environment = SubResource( 1 )
417 | __meta__ = {
418 | "_editor_description_": "Default environment"
419 | }
420 |
421 | [node name="Spatial" type="Spatial" parent="."]
422 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -64, 0, -64 )
423 |
424 | [node name="WaterPlane" type="MeshInstance" parent="Spatial"]
425 | material_override = SubResource( 10 )
426 | skeleton = NodePath("../..")
427 | software_skinning_transform_normals = false
428 | script = ExtResource( 4 )
429 |
430 | [node name="NoiseTexture" type="TextureRect" parent="Spatial/WaterPlane"]
431 | visible = false
432 | margin_left = 107.0
433 | margin_top = 169.0
434 | margin_right = 1131.0
435 | margin_bottom = 1193.0
436 | texture = SubResource( 12 )
437 |
438 | [node name="NoiseNormals" type="TextureRect" parent="Spatial/WaterPlane"]
439 | visible = false
440 | margin_left = -407.0
441 | margin_top = 170.0
442 | margin_right = 617.0
443 | margin_bottom = 1194.0
444 | texture = SubResource( 14 )
445 |
446 | [node name="Terrain" type="MeshInstance" parent="Spatial"]
447 | material_override = SubResource( 5 )
448 | skeleton = NodePath("../..")
449 | software_skinning_transform_normals = false
450 | script = ExtResource( 3 )
451 |
452 | [node name="TestS3D" type="MeshInstance" parent="Spatial"]
453 | material_override = SubResource( 17 )
454 | software_skinning_transform_normals = false
455 | script = ExtResource( 5 )
456 |
457 | [node name="Border" type="MeshInstance" parent="Spatial"]
458 | material_override = SubResource( 8 )
459 | cast_shadow = 2
460 | software_skinning_transform_normals = false
461 |
462 | [node name="TransitTiles" type="MeshInstance" parent="Spatial"]
463 | material_override = SubResource( 19 )
464 | script = ExtResource( 6 )
465 |
466 | [node name="CameraHandler" type="KinematicBody" parent="."]
467 | transform = Transform( -0.924, 0, 0.191, 0, -1, 0.866, 0.383, 1, 0.462, 0, 15.625, 0 )
468 | script = ExtResource( 2 )
469 |
470 | [node name="Camera" type="Camera" parent="CameraHandler"]
471 | projection = 1
472 | current = true
473 | size = 292.0
474 | near = 1.0
475 | far = 200.0
476 |
477 | [node name="WorldEnvironment" type="WorldEnvironment" parent="KinematicBody"]
478 | environment = SubResource( 1 )
479 | __meta__ = {
480 | "_editor_description_": "Default environment"
481 | }
482 |
483 | [node name="UICanvas" type="CanvasLayer" parent="."]
484 |
--------------------------------------------------------------------------------
/CityView/ClassDefinitions/NetGraphEdge.gd:
--------------------------------------------------------------------------------
1 | class_name NetGraphEdge
2 |
3 | var start : Vector2
4 | var end : Vector2
5 | var length : float = 0.0
6 | var tilelocations : PoolVector2Array = PoolVector2Array([])
7 | var start_node : NetGraphNode
8 | var end_node : NetGraphNode
9 |
10 | func _init():
11 | pass
12 |
13 |
--------------------------------------------------------------------------------
/CityView/ClassDefinitions/NetGraphNode.gd:
--------------------------------------------------------------------------------
1 | class_name NetGraphNode
2 |
3 | var location : Vector2
4 | var tilelocations : PoolVector2Array = PoolVector2Array([])
5 | var edges = []
6 |
7 | func _init():
8 | pass
9 |
10 |
--------------------------------------------------------------------------------
/CityView/ClassDefinitions/NetTile.gd:
--------------------------------------------------------------------------------
1 | class_name NetTile
2 |
3 | var locations : Vector2
4 | var edges : Array
5 | var tile : TransitTile
6 | var draw_dir : Vector2
7 |
8 | func _init(location_, edges_, tile_, draw_dir_):
9 | self.locations = location_
10 | self.edges = edges_
11 | self.tile = tile_
12 | self.draw_dir = draw_dir_
13 |
14 |
--------------------------------------------------------------------------------
/CityView/ClassDefinitions/NeworkGraph.gd:
--------------------------------------------------------------------------------
1 | class_name NetworkGraph
2 |
3 | var edges : Dictionary = {}
4 | var nodes : Array
5 | var routes : Dictionary
6 |
7 | func _init():
8 | pass
9 |
10 | func _tiles_to_graph_edges(tiles : Array):
11 | for tilepath in tiles[0].tilepaths:
12 | if not self.edges.keys().has(tilepath.type):
13 | self.edges[tilepath.type] = []
14 | var edge = NetGraphEdge.new()
15 | edge.start = tiles[0].location
16 | edge.end = tiles[-1].location
17 | for tile in tiles:
18 | edge.length += tilepath.length
19 | edge.tilelocations.append(tile.location)
20 | if not tile.networedges.keys().has(tilepath.type):
21 | tile.networedges[tilepath.type] = []
22 | tile.networkedges[tilepath.type].append(edge)
23 | self.edges[tilepath.type].append(edge)
24 |
25 |
--------------------------------------------------------------------------------
/CityView/ClassDefinitions/TransitTile.gd:
--------------------------------------------------------------------------------
1 | class_name TransitTile
2 |
3 | var edges : Dictionary
4 | var ids : Dictionary
5 | var tilepaths : Dictionary
6 | var text_arr_layers : Dictionary
7 | var UVs : Dictionary
8 |
9 | func _init(edges_ : Dictionary, ids_ : Dictionary, layers_ : Dictionary):
10 | self.edges = edges_
11 | self.ids = ids_
12 | self.tilepaths = self.set_tile_paths()
13 | self.text_arr_layers = layers_
14 |
15 | func set_UVs(tile_ind : int, dir : Vector2):
16 | var rot = self.ids[tile_ind][1]
17 | var flip = self.ids[tile_ind][2]
18 | """
19 | rotations are declared as clockwise, godot requires counter-clockwise vertex declaration
20 | since flip->rotate =/= rotate->flip and rotations come first in the 3-line declaration
21 | I will assume rotation is applied before flipping
22 | as flip is a bool that doesn't specify the axis to flip along I will assume it flips "horizontally"
23 | along the v axis flipping the u coordinates ^^
24 |
25 | so a rotate=3, flip=1 would perform:
26 | rot flip
27 | 1-4 4-3 3-4
28 | | | -> | | -> | |
29 | 2-3 1-2 2-1
30 | """
31 | var corner_uvs = [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 0)]
32 | var rot_uvs = []
33 | for i in range(rot, 4+rot, 1):
34 | rot_uvs.append(corner_uvs[i%4])
35 | var flip_uvs = []
36 | if flip == 1:
37 | flip_uvs.append(rot_uvs[3])
38 | flip_uvs.append(rot_uvs[2])
39 | flip_uvs.append(rot_uvs[1])
40 | flip_uvs.append(rot_uvs[0])
41 | else:
42 | flip_uvs = rot_uvs
43 | # will need to use the same order when assigning vectors
44 | var ret_uvs = PoolVector2Array([
45 | flip_uvs[0],
46 | flip_uvs[2],
47 | flip_uvs[1],
48 | flip_uvs[0],
49 | flip_uvs[3],
50 | flip_uvs[2]
51 | ])
52 | if (dir.x>0 and dir.y>0) or (dir.x<0 and dir.y<0):
53 | ret_uvs = PoolVector2Array([
54 | flip_uvs[0],
55 | flip_uvs[3],
56 | flip_uvs[1],
57 | flip_uvs[3],
58 | flip_uvs[2],
59 | flip_uvs[1]
60 | ])
61 | self.UVs[[tile_ind, dir]] = ret_uvs
62 |
63 | func set_tile_paths():
64 | """
65 | get path from SC4Path file (still needs parsing)
66 | """
67 | return {}
68 |
--------------------------------------------------------------------------------
/CityView/Meshes/Terrain.gd:
--------------------------------------------------------------------------------
1 | extends MeshInstance
2 |
3 | var tmpMesh = ArrayMesh.new();
4 | var vertices = PoolVector3Array()
5 | var UVs = PoolVector2Array()
6 | var color = Color(0.9, 0.1, 0.1)
7 | var mat = self.get_material_override()
8 | var st = SurfaceTool.new()
9 | var tm_table
10 | var heightmap : Array
11 |
12 | func _ready():
13 | #mat.albedo_color = color
14 | st.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)
15 | for v in vertices.size():
16 | #st.add_color(color)
17 | st.add_uv(UVs[v])
18 | st.add_vertex(vertices[v])
19 |
20 | st.commit(tmpMesh)
21 | self.set_mesh(tmpMesh)
22 |
23 |
24 |
25 | func load_into_config_file(file):
26 | # In Godot there can't be used ConfigFile initially
27 | # A custom parser has to be there
28 | # Then the ConfigFile is build
29 | var ini_str = file.raw_data.get_string_from_ascii()
30 |
31 | # Uncomment to see raw data in file - for debug purpose only
32 | #var file2 = File.new()
33 | #file2.open("user://terrain_params.ini", File.WRITE)
34 | #file2.store_string(ini_str)
35 | #file2.close()
36 | var configFile = ConfigFile.new()
37 | var current_section = ''
38 | for line in ini_str.split('\n'):
39 | line = line.strip_edges(true, true)
40 | if line.length() == 0:
41 | continue
42 | if line[0] == '#' or line[0] == ';':
43 | continue
44 | if line[0] == '[':
45 | current_section = line.substr(1, line.length() - 2)
46 | else:
47 | var key = line.split('=')[0]
48 | var value = line.split('=')[1]
49 | configFile.set_value(current_section,key,value)
50 | return configFile
51 |
52 | func remove_comma_at_end_of_line(line):
53 | line = line.left(line.length() - 1)
54 | return line
55 |
56 | func read_textures_numbers_and_build_tm_table(config):
57 | tm_table = []
58 | var textures = [17, 16, 21]
59 |
60 | var keys = config.get_section_keys("TropicalMiscTextures")
61 | for key in keys:
62 | if key == "LowCliff" or key == "Beach":
63 | textures.append(config.get_value("TropicalMiscTextures", key).hex_to_int())
64 |
65 | keys = config.get_section_keys("TropicalTextureMapTable")
66 | for key in keys:
67 | var value = config.get_value("TropicalTextureMapTable", key)
68 | value = remove_comma_at_end_of_line(value)
69 | var numbers = value.split(",")
70 | var list_of_numbers = []
71 | for number in numbers:
72 | number = number.hex_to_int()
73 | list_of_numbers.append(number)
74 | if not textures.has(number):
75 | textures.append(number)
76 | tm_table.append(list_of_numbers)
77 | return textures
78 |
79 |
80 | func build_image_dict_and_texture_array(textures):
81 | # This function will create a dictionary with images(textures) and
82 | var type_tex = 0x7ab50e44
83 | var group_tex = 0x891B0E1A
84 | var images_dict = {}
85 | var max_width = 0
86 | var height = 0
87 | var list_texture_format = []
88 | for texture in textures:
89 | for zoom in range(5):
90 | # Why such a calculation?
91 | var zoom_id = texture + (zoom * 256)
92 | var fsh_subfile = Core.subfile(type_tex, group_tex, zoom_id, FSHSubfile)
93 | # Check the length of data
94 | if len(fsh_subfile.img.data["data"]) == 0:
95 | Logger.error("Invalid SFH")
96 | # Add the texture format to the list if is not yet there
97 | var texture_format = fsh_subfile.img.get_format()
98 | if not list_texture_format.has(texture_format):
99 | list_texture_format.append(texture_format)
100 | # Add the image to the dict based on zoom_id
101 | images_dict[zoom_id] = fsh_subfile
102 | # Search for the most wide texture and save also its height
103 | if fsh_subfile.width > max_width:
104 | max_width = fsh_subfile.width
105 | height = fsh_subfile.height
106 |
107 | var texture_array = TextureArray.new()
108 | texture_array.create (max_width, height, len(textures) * 5, list_texture_format[0], 2)
109 | return {
110 | "texture_array":texture_array,
111 | "images_dict": images_dict
112 | }
113 |
114 | func create_ind_to_layer(config, images_dict, texture_array):
115 | var dict = {}
116 | var layer = 0
117 | var cliff_index = config.get_value("TropicalMiscTextures", "LowCliff").hex_to_int()
118 | var beach_index = config.get_value("TropicalMiscTextures", "Beach").hex_to_int()
119 | var top_edge
120 | var mid_edge
121 | var bot_edge
122 |
123 | for key in images_dict:
124 | var image = images_dict[key].img
125 | texture_array.set_layer_data(image, layer)
126 | if key < 256:
127 | dict[key] = layer
128 | if key == cliff_index:
129 | cliff_index = layer
130 | elif key == beach_index:
131 | beach_index = layer
132 | elif key == 17:
133 | top_edge = layer
134 | elif key == 16:
135 | mid_edge = layer
136 | elif key == 21:
137 | bot_edge = layer
138 | var test = texture_array.get_layer_data(layer)
139 | if len(test.data["data"]) == 0:
140 | Logger.error("failed to load layer %s with image %s" % [layer, key])
141 | layer += 1
142 |
143 | self.mat.set_shader_param("cliff_ind", float(cliff_index))
144 | self.mat.set_shader_param("beach_ind", float(beach_index))
145 | self.mat.set_shader_param("terrain", texture_array)
146 | self.set_material_override(self.mat)
147 | var mat_e = self.get_parent().get_node("Border").get_material_override()
148 | mat_e.set_shader_param("terrain", texture_array)
149 |
150 | mat_e.set_shader_param("top_ind", float(top_edge))
151 | mat_e.set_shader_param("mid_ind", float(mid_edge))
152 | mat_e.set_shader_param("bot_ind", float(bot_edge))
153 |
154 | return dict
155 |
156 | func load_textures_to_uv_dict():
157 | # Output from this function is
158 | # tm_table
159 | # ind_to_layer - This is the UV dictionary?
160 | # mat
161 | # self
162 |
163 | # Load file from SubFile
164 | var type_ini = 0x00000000
165 | var group_ini = 0x8a5971c5
166 | var instance_ini = 0xAA597172
167 | var file = Core.subfile(type_ini, group_ini, instance_ini, DBPFSubfile)
168 |
169 | # File - Read parameters related to the terrain
170 | var config = load_into_config_file(file)
171 |
172 | # See how the data actually looks like - comment next line of not DEBUG
173 | #config.save("user://new.ini")
174 |
175 | var textures = read_textures_numbers_and_build_tm_table(config)
176 |
177 | var results = build_image_dict_and_texture_array(textures)
178 |
179 | var ind_to_layer = create_ind_to_layer(config, results.images_dict, results.texture_array)
180 |
181 | #var f = File.new()
182 | #f.open("user://uv_dict.txt", File.WRITE)
183 | #f.store_line(to_json(ind_to_layer))
184 | #f.close()
185 |
186 | return ind_to_layer
187 |
188 | func update_terrain(locations : PoolVector3Array, rot_flipped_UVs : PoolVector2Array):
189 | var neighbours = [
190 | Vector3(-1, 0, -1),Vector3(0, 0, -1),Vector3(1, 0, -1),
191 | Vector3(-1, 0, 0),Vector3(0, 0, 0),Vector3(1, 0, 0),
192 | Vector3(-1, 0, 1),Vector3(0, 0, 1),Vector3(1, 0, 1)
193 | ]
194 | var surface_ind = self.mesh.get_surface_count() - 1
195 | if surface_ind != 0:
196 | print("error: terrain somehow has more than one surface!")
197 | var arrays = self.mesh.surface_get_arrays(0).duplicate(true)
198 | self.mesh.surface_remove(0)
199 | var vertices_copy = arrays[ArrayMesh.ARRAY_VERTEX]
200 | var UVs_copy = arrays[ArrayMesh.ARRAY_TEX_UV]
201 | # iterate locations per quad
202 | var updated_vertices = []
203 | for i in range(0, len(locations), 6):
204 | var tile_loc = locations[i]
205 | for neigh in neighbours:
206 | var index = self.get_parent().get_parent().terr_tile_ind[Vector2(tile_loc.x+neigh.x, tile_loc.z+neigh.z)]
207 | # update vertices per quad
208 | for j in range(6):
209 | for k in range(6):
210 | if vertices_copy[index+j].x == locations[i+k].x and vertices_copy[index+j].z == locations[i+k].z:
211 | vertices_copy[index+j] = locations[i+k] - Vector3(0.0, 0.01, 0.0)
212 | # Vertex order can be different, therefore UVs needs updating
213 | if neigh == Vector3(0,0,0):
214 | UVs_copy[index+j] = rot_flipped_UVs[i+j]
215 |
216 | arrays[ArrayMesh.ARRAY_VERTEX] = vertices_copy
217 | arrays[ArrayMesh.ARRAY_TEX_UV] = UVs_copy
218 | self.mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
219 |
220 |
221 |
222 | # Called every frame. 'delta' is the elapsed time since the previous frame.
223 | #func _process(delta):
224 | # pass
225 |
--------------------------------------------------------------------------------
/CityView/Meshes/TestS3D.gd:
--------------------------------------------------------------------------------
1 | extends MeshInstance
2 |
3 | var tmpMesh = ArrayMesh.new();
4 | var vertices = PoolVector3Array()
5 | var UVs = PoolVector2Array()
6 | var color = Color(0.9, 0.1, 0.1)
7 | var mat = self.get_material_override()
8 | var st = SurfaceTool.new()
9 | var tm_table
10 |
11 | # Called when the node enters the scene tree for the first time.
12 | func _ready():
13 |
14 | #mat.albedo_color = color
15 | st.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)
16 | for v in vertices.size():
17 | #st.add_color(color)
18 | st.add_uv(UVs[v])
19 | st.add_vertex(vertices[v])
20 |
21 | st.commit(tmpMesh)
22 | self.set_mesh(tmpMesh)
23 |
24 |
25 | # Called every frame. 'delta' is the elapsed time since the previous frame.
26 | #func _process(delta):
27 | # pass
28 |
--------------------------------------------------------------------------------
/CityView/Meshes/WaterPlane.gd:
--------------------------------------------------------------------------------
1 | extends MeshInstance
2 |
3 | var tmpMesh = ArrayMesh.new();
4 | var vertices = PoolVector3Array()
5 | var UVs = PoolVector2Array()
6 | var color = Color(0.9, 0.1, 0.1)
7 | var mat = self.get_material_override()
8 | var st = SurfaceTool.new()
9 | var tm_table
10 | # Declare member variables here. Examples:
11 | # var a = 2
12 | # var b = "text"
13 |
14 |
15 | # Called when the node enters the scene tree for the first time.
16 | func _ready():
17 |
18 |
19 | #mat.albedo_color = color
20 | st.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)
21 | for v in vertices.size():
22 | #st.add_color(color)
23 | st.add_uv(UVs[v])
24 | st.add_vertex(vertices[v])
25 |
26 | st.commit(tmpMesh)
27 | self.set_mesh(tmpMesh)
28 |
29 | func generate_wateredges(HeightMap):
30 | var TGI_Tprop = {"T": 0x6534284a, "G": 0x88cd66e9, "I":0x00000001}
31 | var _Terr_properties = Core.subfile(TGI_Tprop["T"], TGI_Tprop["G"], TGI_Tprop["I"], DBPFSubfile)
32 | "TODO figure out how to read exemplar files"
33 | var waterheight = 250.0
34 | var beachrange = 2
35 | var max_beach_height = 4.0 # this seems to be ignored
36 | var max_depth_water_alpha = 30.0
37 |
38 | var depth_range = max_depth_water_alpha + max_beach_height
39 | var watermap = PoolByteArray([])
40 | var watercoords = []
41 | for w in range(len(HeightMap)):
42 | watercoords.append([])
43 | for h in range(len(HeightMap[0])):
44 | if HeightMap[w][h] < waterheight:
45 | var m_y = (HeightMap[w][h] - waterheight)
46 | if m_y >= -max_depth_water_alpha:
47 | var nearness = ((depth_range-(m_y+max_depth_water_alpha))/depth_range)*255.0
48 | watermap.append_array([nearness, 0.0, 0.0, 0.0])
49 | else:
50 | watermap.append_array([255.0, 0.0, 0.0, 0.0])
51 | # scan neighbours
52 | var min_w = max(w - 1, 0.0)
53 | var max_w = min(w + 2, len(HeightMap))
54 | var min_h = max(h - 1, 0.0)
55 | var max_h = min(h + 2, len(HeightMap[0]))
56 | var found = false
57 | for n_w in range(min_w, max_w):
58 | for n_h in range(min_h, max_h):
59 | if HeightMap[n_w][n_h] > waterheight:
60 | watercoords[w].append(h)
61 | found = true
62 | break
63 | if found:
64 | break
65 | else:
66 | watermap.append_array([0.0, 0.0, 0.0, 0.0])
67 | var max_dist = sqrt(pow(beachrange+3, 2)*2)
68 | for w in range(len(watercoords)):
69 | for h in watercoords[w]:
70 | var min_w = max(w - (beachrange+3), 0.0)
71 | var max_w = min(w + (beachrange+4), len(HeightMap))
72 | var min_h = max(h - (beachrange+3), 0.0)
73 | var max_h = min(h + (beachrange+4), len(HeightMap[0]))
74 | for neigh_w in range(min_w, max_w):
75 | for neigh_h in range(min_h, max_h):
76 | var dist_str = (((1.0 - sqrt(pow(neigh_w - w, 2) + pow(neigh_h - h, 2))/max_dist) * max_beach_height)/depth_range)*255.0
77 | if dist_str > watermap[(neigh_h + (neigh_w * len(HeightMap)))*4]:
78 | watermap[(neigh_h + (neigh_w * len(HeightMap)))*4] = dist_str
79 | """var neigh_y = (HeightMap[neigh_w][neigh_h] - waterheight)
80 |
81 | this range needs to go from -30 to 4
82 |
83 |
84 | if neigh_y <= (max_beach_height) and neigh_y >= -max_depth_water_alpha:
85 | var nearness = ((depth_range-(neigh_y+max_depth_water_alpha))/depth_range)*255.0
86 | watermap[(neigh_h + (neigh_w * len(HeightMap)))*4] = nearness"""
87 | """
88 | this nearness results in a blocky effect,
89 | preferable is a circular neighbor search where
90 | if its value is less than new_value it gets overwritten
91 | with new value being dependant on its radial distance from current coord
92 | water tiles should be height dependant up to 30m depth
93 | """
94 | var TGI_waterT = {"T": 0x7ab50e44, "G": 0x891b0e1a, "I":0x09187300}
95 | var water_imgs = []
96 | for zoom in range(5):
97 | water_imgs.append(Core.subfile(TGI_waterT["T"], TGI_waterT["G"], TGI_waterT["I"]+zoom, FSHSubfile))
98 |
99 |
100 | var water_text = TextureArray.new()
101 | var w_w = water_imgs[4].width
102 | var w_h = water_imgs[4].height
103 | var format = water_imgs[4].img.get_format()
104 | water_text.create (w_h, w_w, 5, format, 2)
105 | for i in range(len(water_imgs)):
106 | water_text.set_layer_data(water_imgs[i].img, i)
107 | var watermap_inv =[]
108 | for w in range(len(HeightMap)):
109 |
110 | for h in range(len(HeightMap[0])):
111 | var i = h * len(HeightMap)
112 | var j = (i + w)*4
113 | watermap_inv.append(watermap[j])
114 | watermap_inv.append(watermap[j-1])
115 | watermap_inv.append(watermap[j-2])
116 | watermap_inv.append(watermap[j-3])
117 |
118 | var shoreimg = Image.new()
119 | shoreimg.create_from_data(len(HeightMap), len(HeightMap[0]), false, Image.FORMAT_RGBA8, watermap_inv)
120 | #shoreimg.flip_x()
121 | #shoreimg.flip_y()
122 | var shoretex = ImageTexture.new()
123 | shoretex.create_from_image(shoreimg, 0)
124 | mat = self.get_material_override()
125 | mat.set_shader_param("watermap", shoretex)
126 | mat.set_shader_param("watertexture", water_text)
127 | mat.set_shader_param("depth_range", depth_range)
128 | mat.set_shader_param("max_depth", max_depth_water_alpha)
129 | mat.set_shader_param("noise_texture", $NoiseTexture.texture)
130 | mat.set_shader_param("noise_normals", $NoiseNormals.texture)
131 | self.set_material_override(mat)
132 | var matT = self.get_parent().get_node("Terrain").get_material_override()
133 | matT.set_shader_param("watermap", shoretex)
134 | matT.set_shader_param("max_beach_ht", max_beach_height)
135 | matT.set_shader_param("beach_ht_range", depth_range)
136 | self.get_parent().get_node("Terrain").set_material_override(matT)
137 |
138 |
139 | # Called every frame. 'delta' is the elapsed time since the previous frame.
140 | #func _process(delta):
141 | # pass
142 |
--------------------------------------------------------------------------------
/Core.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends Node
18 |
19 | var subfile_indices : Dictionary
20 | # TODO: read region_settings from file
21 | var region_settings : Dictionary = {
22 | "show_borders" : true,
23 | "show_names" : true,
24 | "view_mode" : "satellite",
25 | }
26 | var sub_by_type_and_group : Dictionary
27 | var dbpf_files: Dictionary
28 | var game_dir = null
29 |
30 | var type_dict_to_text = {
31 | 0x6534284a: "exemplar",
32 | 0x2026960b: "LTEXT",
33 | 0x5ad0e817: "S3D",
34 | 0x05342861: "cohorts",
35 | 0x29a5d1ec: "ATC",
36 | 0x09ADCD75: "AVP",
37 | 0x7ab50e44: "FSH",
38 | 0xea5118b0: "EFFDIR",
39 | 0x856ddbac: "PNG",
40 | 0xca63e2a3: "LUA",
41 | 0xe86b1eef: "DBDF",
42 | 0x00000000: "text",
43 | 0x0a5bCf4b: "RUL",
44 | 0xaa5c3144: "Cursor",
45 | 0xa2e3d533: "KeyCursor",
46 | 0x296678f7: "SC4Path",
47 | }
48 |
49 | # This dictionary should actually be a dictionary of dictionaries (type -> group)
50 | var group_dict_to_text = {
51 | 0x00000001: "VIDEO,BW_CURSOR",
52 | 0x46a006b0: "UI_IMAGE",
53 | 0x1abe787d: "UI_IMAGE2",
54 | 0x22dec92d: "UI_TOOLSIMAGE",
55 | 0x8a5971c5: "UDI_SOUNDS_DATA",
56 | 0x2a2458f9: "PROPS_ANIM",
57 | 0x49a593e7: "NONPROPS_ANIM",
58 | 0xaa5bcf57: "BRIDGE_RULES",
59 | 0x49dd6e08: "CONFIG",
60 | 0x2a4d1937: "EFFECTS1",
61 | 0x2a4d193d: "EFFECTS2",
62 | 0xaa4d1920: "EFFECTS3",
63 | 0xaa4d1930: "EFFECTS4",
64 | 0xaa4d1933: "EFFECTS5",
65 | 0xca4d1943: "EFFECTS6",
66 | 0xca4d1948: "EFFECTS7",
67 | 0xea4d192a: "EFFECTS8",
68 | 0x4a54e387: "SUBWAY_VIEW",
69 | 0x4a42c073: "TRAFFIC_MEDIUM",
70 | 0x0a4d1926: "SCHOOL_BELL_FIRE_ENGINES",
71 | 0x6a4d193a: "FIRE_OBLITERATE",
72 | 0x9dbdbf74: "SOUND_HITLISTS",
73 | 0x4a4d1946: "PLOP_BUTTON_CLICK_DECAY_WIRE_FIRE_TOOLS",
74 | 0xca88cc85: "ABANDONED",
75 | 0xcb6b7bd9: "LE_ARROW_IMAGE",
76 | 0x891b0e1a: "TERRAIN_FOUNDATION",
77 | 0x2bc2759a: "TRANSIT_NETWORK_SHADOW",
78 | 0x0986135e: "BASE_OVERLAY",
79 | 0xbadb57f1: "SIMGLIDE",
80 | 0x69668828: "TEXTURED_NETWORK_PATH",
81 | 0xa966883f: "3D_NETWORK_PATH",
82 | 0x6a386d26: "MENU_ICONS",
83 | 0x89ac5643: "EXEMPLAR_TRANSIT_PIECES",
84 | 0x67bddf0c: "ZONABLE_RESIDENTIAL_BUILDING_PARENTS",
85 | 0x690f693f: "DATA_VIEW_PARENTS",
86 | 0x6a297266: "MY_SIM_PARENT",
87 | 0x7a4a8458: "CLOUDS_PARENT",
88 | 0x47bddf12: "DEVELOPER_COMMERCIAL",
89 | 0x96a006b0: "UI_XML",
90 | 0x08000600: "UI_800x600",
91 | 0x0a554af5: "LTEXT/Audio UI Panel texts",
92 | 0x0a554ae8: "LTEXT/General UI texts",
93 | 0x0a554ae0: "LTEXT/Item visible name and description texts",
94 | 0x0a419226: "LTEXT/In game error texts",
95 | 0x2a592fd1: "LTEXT/Item plop/draw notification texts",
96 | 0x4a5e093c: "LTEXT/Terrain tool texts",
97 | 0x4a5cb171: "LTEXT/Funny random city loading message texts",
98 | 0x6a231eaa: "LTEXT/Interactivity Feature Texts (MySim, UDriveIt, etc.)",
99 | 0x6a231ea4: "LTEXT/News ticker message texts",
100 | 0x6a3ff01c: "LTEXT/Game UI Texts",
101 | 0x6a4eb3f7: "LTEXT/Population Text",
102 | 0x6a554afd: "LTEXT/Misc. Item Names/Descriptions (2)",
103 | 0x8a635d24: "LTEXT/Audio filename to description texts",
104 | 0x8a5e03ec: "LTEXT/Disaster texts",
105 | 0x8a4924f3: "LTEXT/About SC4 window HTML text",
106 | 0xca554b03: "LTEXT/Popup window HTML texts",
107 | 0xea231e96: "LTEXT/Misc. Texts",
108 | 0xea5524eb: "LTEXT/Misc. Item Names/Descriptions (1)",
109 | 0xeafcb180: "LTEXT/Plugin Install Text",
110 | }
111 |
112 |
113 | # TODO Generate these dictionaries from the above
114 | var type_dict = {
115 | "LTEXT": 0x2026960b,
116 | "S3D": 0x5ad0e817,
117 | "Cohorts" : 0x05342861,
118 | "ATC": 0x29a5d1ec,
119 | "AVP": 0x09ADCD75,
120 | "FSH": 0x7ab50e44,
121 | "EFFDIR": 0xea5118b0,
122 | "PNG": 0x856ddbac,
123 | "LUA": 0xca63e2a3,
124 | "DBDF": 0xe86b1eef,
125 | "TEXT": 0x00000000,
126 | }
127 | var group_dict = {
128 | "VIDEO,BW_CURSOR": 0x00000001,
129 | "UI_IMAGE": 0x46a006b0,
130 | "UI_IMAGE2": 0x1ABE787D,
131 | "UI_TOOLSIMAGE": 0x22DEC92D,
132 | "UDI_SOUNDS_DATA": 0x8a5971c5,
133 | "PROPS_ANIM": 0x2a2458f9,
134 | "NONPROPS_ANIM": 0x49a593e7,
135 | "BRIDGE_RULES": 0xaa5bcf57,
136 | "CONFIG": 0x49dd6e08,
137 | "EFFECTS1": 0x2a4d1937,
138 | "EFFECTS2": 0x2a4d193d,
139 | "EFFECTS3": 0xaa4d1920,
140 | "EFFECTS4": 0xaa4d1930,
141 | "EFFECTS5": 0xaa4d1933,
142 | "EFFECTS6": 0xca4d1943,
143 | "EFFECTS7": 0xca4d1948,
144 | "EFFECTS8": 0xea4d192a,
145 | "SUBWAY_VIEW": 0x4a54e387,
146 | "TRAFFIC_MEDIUM": 0x4a42c073,
147 | "SCHOOL_BELL_FIRE_ENGINES": 0x0a4d1926,
148 | "FIRE_OBLITERATE": 0x6a4d193a,
149 | "SOUND_HITLISTS": 0x9dbdbf74,
150 | "PLOP_BUTTON_CLICK_DECAY_WIRE_FIRE_TOOLS": 0x4a4d1946,
151 | "ABANDONED": 0xca88cc85,
152 | "LE_ARROW_IMAGE": 0xCB6B7BD9,
153 | "TERRAIN_FOUNDATION": 0x891B0E1A,
154 | "TRANSIT_NETWORK_SHADOW": 0x2BC2759A,
155 | "BASE_OVERLAY": 0x0986135E,
156 | "SIMGLIDE": 0xbadb57f1,
157 | "TEXTURED_NETWORK_PATH": 0x69668828,
158 | "3D_NETWORK_PATH": 0xa966883f,
159 | "MENU_ICONS": 0x6a386d26,
160 | "EXEMPLAR_TRANSIT_PIECES": 0x89ac5643,
161 | "ZONABLE_RESIDENTIAL_BUILDING_PARENTS": 0x67bddf0c,
162 | "DATA_VIEW_PARENTS": 0x690f693f,
163 | "MY_SIM_PARENT": 0x6a297266,
164 | "CLOUDS_PARENT": 0x7a4a8458,
165 | "DEVELOPER_COMMERCIAL": 0x47bddf12,
166 | "UI_XML": 0x96a006b0,
167 | "UI_800x600": 0x08000600
168 | }
169 | var class_dict = {
170 | "LTEXT": null,
171 | "S3D": null,
172 | "Cohorts" : null,
173 | "ATC": null,
174 | "AVP": null,
175 | "FSH": FSHSubfile,
176 | "EFFDIR": null,
177 | "PNG": ImageSubfile,
178 | "LUA": null,
179 | "DBDF": DBPFSubfile,
180 | "TEXT": null
181 | }
182 |
183 | func _type_int_2_str(dict, number:int) -> String:
184 | """
185 | Tries to number into text based on dicionary
186 | Returns empty string if not found.
187 | """
188 | var result : String
189 | var keys = dict.keys()
190 | if dict.has(number):
191 | result = dict[number]
192 | return result
193 |
194 |
195 | func _type_str_2_int(dict, text: String) -> int:
196 | """
197 | Tries to translate string into number based on dicionary
198 | Return 0 if not found
199 | """
200 | var number = 0
201 | if dict.has(text):
202 | number = dict[text]
203 | else:
204 | Logger.error("Could not translate %s into number. Not found." % text)
205 | return number
206 |
207 |
208 | func get_list_instances(type_id_str:String, group_id_str: String):
209 | """
210 | Provides list of all instances based on given type_id and group_id.
211 | type_id and group_id are first translated from String to int.
212 | The instances are taken from subfile_indices.
213 | """
214 | var type_id = self._type_str_2_int(self.type_dict, type_id_str)
215 | var group_id = self._type_str_2_int(self.group_dict, group_id_str)
216 |
217 | var instances_list = []
218 | for key in subfile_indices.keys():
219 | if key[0] == type_id and key[1] == group_id:
220 | instances_list.append(key[2])
221 | return instances_list
222 |
223 | func get_list_groups(type_id_str:String):
224 | """
225 | Based on type_id it return list of all groups assosiated with this type_id.
226 | The type_id is first translated to the number and then a search is performed.
227 | It provides two outputs first is a list of groups in numbers and second
228 | same list of groups but names are put there where they are known.
229 | """
230 | var type_id = self._type_str_2_int(self.type_dict, type_id_str)
231 | var groups_list = []
232 | # Collect the groups list
233 | for key in subfile_indices.keys():
234 | if key[0] == type_id and not groups_list.has(key[1]):
235 | groups_list.append(key[1])
236 | # Try to put names there
237 | var names = []
238 | for item in groups_list:
239 | var name = _type_int_2_str(self.group_dict_to_text,item)
240 | if not name:
241 | names.append("0x%08x" % item)
242 | else:
243 | names.append(name)
244 | return {
245 | "groups_list" : groups_list,
246 | "groups_names" : names,
247 | }
248 |
249 | func get_subfile(type_id_str: String, group_id_str: String, instance_id : int) -> DBPFSubfile:
250 | """
251 | This user friendly function like subfile.
252 | Input arguments are String and they are translated to numbers first.
253 | Then subfile function is called.
254 | """
255 | var type_id = self._type_str_2_int(self.type_dict, type_id_str)
256 | var group_id = self._type_str_2_int(self.group_dict, group_id_str)
257 | var class_type = self._type_str_2_int(self.class_dict, type_id_str)
258 | var subfile = subfile(type_id, group_id, instance_id, class_type)
259 | return subfile
260 |
261 |
262 | func subfile(type_id : int, group_id : int, instance_id : int, subfile_class) -> DBPFSubfile:
263 | if not subfile_indices.has(SubfileTGI.TGI2str(type_id, group_id, instance_id)):
264 | Logger.error("Unknown subfile %s" % SubfileTGI.get_file_type(type_id, group_id, instance_id))
265 | return null
266 | else:
267 | var index = subfile_indices[SubfileTGI.TGI2str(type_id, group_id, instance_id)]
268 | return index.dbpf.get_subfile(type_id, group_id, instance_id, subfile_class)
269 |
270 | func add_dbpf(dbpf : DBPF):
271 | if not dbpf_files.has(dbpf.path):
272 | dbpf_files[dbpf.path] = dbpf
273 | for ind_key in dbpf.indices.keys():
274 | var index = dbpf.indices[ind_key]
275 | # Don't report DBDF "overwrite" with the type id
276 | if subfile_indices.has(ind_key) and index.type_id != 0xe86b1eef: # and not (index.type_id == "DBPF" and index.group_id == 0xe86b1eef and index.instance_id == 0x286b1f03):
277 | Logger.error("File '%s' overwrites subfile %s" % [dbpf.path, SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id)])
278 | subfile_indices[ind_key] = dbpf.indices[ind_key]
279 | if not sub_by_type_and_group.keys().has([index.type_id, index.group_id]):
280 | sub_by_type_and_group[[index.type_id, index.group_id]] = {}
281 | sub_by_type_and_group[[index.type_id, index.group_id]][index.instance_id] = (dbpf.indices[ind_key])
282 |
283 | func get_gamedata_path(path: String) -> String:
284 | return "%s/%s" % [Core.game_dir, path]
285 |
--------------------------------------------------------------------------------
/DATExplorer/DATExplorer.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends VBoxContainer
18 |
19 | var dbpf_files_src = []
20 | var tree : Tree
21 | var file_treeitems : Dictionary
22 | # Avoid loading all the files at once, instead group them by type IDs using these dictionaries of dictionaries
23 | var typeid_count : Dictionary
24 | var typeid_treeitems : Dictionary
25 | var filter_type_mask : int = 0xFFFFFFFF
26 | var filter_type_id : int = 0
27 | var filter_group_mask : int = 0xFFFFFFFF
28 | var filter_group_id : int = 0
29 | var filter_instance_mask : int = 0xFFFFFFFF
30 | var filter_instance_id : int = 0
31 |
32 |
33 | func _ready():
34 | tree = $DATTree
35 | base_load()
36 | $Filters.visible = true
37 | $DATTree.visible = true
38 | $SubfilePreview.visible = true
39 | $Filters/Type/Label.text = "Type filter"
40 | $Filters/Group/Label.text = "Group filter"
41 | $Filters/Instance/Label.text = "Instance filter"
42 |
43 | func base_load():
44 | var root = tree.create_item()
45 | tree.set_hide_root(true)
46 | # Create all subtrees for each type ID
47 | # We assume dbpf_files is already filled with the files full paths
48 | # we use full paths to avoid clashing between two files with the same name in different directories
49 | for dbpf in Core.dbpf_files.values():
50 | Logger.info("Processing %s" % dbpf.path)
51 | typeid_count[dbpf.path] = {}
52 | typeid_treeitems[dbpf.path] = {}
53 | var file_treeitem = tree.create_item(root)
54 | file_treeitem.set_text(0, dbpf.path.get_file())
55 | file_treeitem.set_text(1, "%d subfiles" % dbpf.indices.size())
56 | file_treeitem.collapsed = true
57 | file_treeitems[dbpf.path] = file_treeitem
58 | for i in range(4):
59 | file_treeitem.set_selectable(i, false)
60 | for index in dbpf.indices.values():
61 | var type_id = index.type_id
62 | if typeid_count[dbpf.path].has(type_id):
63 | typeid_count[dbpf.path][type_id] += 1
64 | else:
65 | typeid_count[dbpf.path][type_id] = 1
66 | var type_id_tree_item = tree.create_item(file_treeitem)
67 | type_id_tree_item.set_text(0, Core.type_dict_to_text.get(type_id, "unknown type"))
68 | type_id_tree_item.set_text(1, "0x%08x" % type_id)
69 | type_id_tree_item.set_text(2, "%d" % dbpf.indices_by_type[type_id].size())
70 | typeid_treeitems[dbpf.path][type_id] = type_id_tree_item
71 | type_id_tree_item.collapsed = true
72 | for i in range(4):
73 | type_id_tree_item.set_selectable(i, false)
74 | # In each tree_item, write the amount of subfiles of that type
75 | for type_id in typeid_count[dbpf.path].keys():
76 | typeid_treeitems[dbpf.path][type_id].set_text(2, "%s subfiles" % typeid_count[dbpf.path][type_id])
77 | Logger.info("Done loading all files")
78 |
79 | func check_filter(index : SubfileIndex) -> bool:
80 | return (index.type_id & filter_type_mask) == filter_type_id & filter_type_mask\
81 | and\
82 | (index.group_id & filter_group_mask) == filter_group_id & filter_group_mask\
83 | and\
84 | (index.instance_id & filter_instance_mask) == filter_instance_id & filter_instance_mask
85 |
86 | func add_subfile_to_tree(dbpf : DBPF, index : SubfileIndex) -> void:
87 | if typeid_treeitems[dbpf.path].has(index.type_id) == false:
88 | Logger.error("Type ID %d not found in file %s" % [index.type_id, dbpf.path])
89 | return
90 | var child = tree.create_item(typeid_treeitems[dbpf.path][index.type_id])
91 | child.set_text(0, SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id))
92 | child.set_text(1, "0x%08x" % index.type_id)
93 | child.set_text(2, "0x%08x" % index.group_id)
94 | child.set_text(3, "0x%08x" % index.instance_id)
95 |
96 | func _on_ApplyFilter_pressed():
97 | filter_type_id = ("0x%s" % $Filters/Type/ID.text).hex_to_int()
98 | filter_type_mask = ("0x%s" % $Filters/Type/Mask.text).hex_to_int()
99 | filter_group_id = ("0x%s" % $Filters/Group/ID.text).hex_to_int()
100 | filter_group_mask = ("0x%s" % $Filters/Group/Mask.text).hex_to_int()
101 | filter_instance_id = ("0x%s" % $Filters/Instance/ID.text).hex_to_int()
102 | filter_instance_mask = ("0x%s" % $Filters/Instance/Mask.text).hex_to_int()
103 |
104 | # Debug: log the filters to check conversion is correct
105 | Logger.info("Filter type ID: 0x%08x" % filter_type_id)
106 | Logger.info("Filter type mask: 0x%08x" % filter_type_mask)
107 | Logger.info("Filter group ID: 0x%08x" % filter_group_id)
108 | Logger.info("Filter group mask: 0x%08x" % filter_group_mask)
109 | Logger.info("Filter instance ID: 0x%08x" % filter_instance_id)
110 | Logger.info("Filter instance mask: 0x%08x" % filter_instance_mask)
111 |
112 | # Clear the files in the tree
113 | for dbpf in Core.dbpf_files.values():
114 | for type_id in dbpf.indices_by_type.keys():
115 | while true:
116 | var child = typeid_treeitems[dbpf.path][type_id].get_children()
117 | if child == null:
118 | break
119 | else:
120 | child.free()
121 |
122 | for dbpf in Core.dbpf_files.values():
123 | for index in dbpf.indices.values():
124 | if check_filter(index):
125 | add_subfile_to_tree(dbpf, index)
126 |
127 | func _on_DATTree_item_selected():
128 | var item = tree.get_selected()
129 | var type_id = item.get_text(1).hex_to_int()
130 | var group_id = item.get_text(2).hex_to_int()
131 | var instance_id = item.get_text(3).hex_to_int()
132 | $SubfilePreview.display_subfile(type_id, group_id, instance_id)
133 |
--------------------------------------------------------------------------------
/DATExplorer/DATExplorer.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=4 format=2]
2 |
3 | [ext_resource path="res://DATExplorer/DATExplorer.gd" type="Script" id=1]
4 | [ext_resource path="res://DATExplorer/Filter.tscn" type="PackedScene" id=3]
5 | [ext_resource path="res://SubfilePreview.tscn" type="PackedScene" id=4]
6 |
7 | [node name="DATExplorerContainer" type="VBoxContainer"]
8 | anchor_left = 0.5
9 | anchor_top = 0.5
10 | anchor_right = 0.5
11 | anchor_bottom = 0.5
12 | margin_left = -640.0
13 | margin_top = -359.0
14 | margin_right = 640.0
15 | margin_bottom = 361.0
16 | script = ExtResource( 1 )
17 |
18 | [node name="DATTree" type="Tree" parent="."]
19 | visible = false
20 | margin_top = 56.0
21 | margin_right = 1280.0
22 | margin_bottom = 306.0
23 | rect_min_size = Vector2( 1, 250 )
24 | columns = 4
25 |
26 | [node name="Filters" type="HBoxContainer" parent="."]
27 | visible = false
28 | margin_right = 1280.0
29 | margin_bottom = 98.0
30 |
31 | [node name="Type" parent="Filters" instance=ExtResource( 3 )]
32 |
33 | [node name="Group" parent="Filters" instance=ExtResource( 3 )]
34 | margin_left = 68.0
35 | margin_right = 132.0
36 |
37 | [node name="Instance" parent="Filters" instance=ExtResource( 3 )]
38 | margin_left = 136.0
39 | margin_right = 200.0
40 |
41 | [node name="ApplyFilter" type="Button" parent="Filters"]
42 | margin_left = 204.0
43 | margin_right = 293.0
44 | margin_bottom = 98.0
45 | text = "Apply filters"
46 |
47 | [node name="SubfilePreview" parent="." instance=ExtResource( 4 )]
48 | visible = false
49 | anchor_right = 0.0
50 | anchor_bottom = 0.0
51 | margin_right = 1280.0
52 | margin_bottom = 14.0
53 |
54 | [connection signal="item_selected" from="DATTree" to="." method="_on_DATTree_item_selected"]
55 | [connection signal="pressed" from="Filters/ApplyFilter" to="." method="_on_ApplyFilter_pressed"]
56 |
--------------------------------------------------------------------------------
/DATExplorer/Filter.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene format=2]
2 |
3 | [node name="TypeFilter" type="VBoxContainer"]
4 | margin_right = 64.0
5 | margin_bottom = 98.0
6 |
7 | [node name="Label" type="Label" parent="."]
8 | margin_right = 64.0
9 | margin_bottom = 14.0
10 | text = "Filter"
11 |
12 | [node name="ID" type="LineEdit" parent="."]
13 | margin_top = 18.0
14 | margin_right = 64.0
15 | margin_bottom = 42.0
16 | max_length = 8
17 | placeholder_text = "id"
18 |
19 | [node name="Mask" type="LineEdit" parent="."]
20 | margin_top = 46.0
21 | margin_right = 64.0
22 | margin_bottom = 70.0
23 | text = "00000000"
24 | max_length = 8
25 | placeholder_text = "mask"
26 |
--------------------------------------------------------------------------------
/DATExplorer/SubfilePreview.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends PanelContainer
18 |
19 | var current_preview : DBPFSubfile = null
20 |
21 | # We presume the file is loaded and available in the `Core` storage
22 | func display_subfile(type_id : int, group_id : int, instance_id : int) -> void:
23 | clear_preview()
24 | var type = Core.type_dict_to_text[type_id]
25 | if type == 'text':
26 | # TODO: also display the source code
27 | if group_id == 0x96A006B0 or group_id == 0x08000600: # UI subfile
28 | Logger.info("Previewing a UI file")
29 | var file = Core.subfile(type_id, group_id, instance_id, SC4UISubfile)
30 | # If the file had already been loaded, then the root won't be null
31 | if file.root != null:
32 | file.root.visible = true
33 | file.add_to_tree($UI, {})
34 | $UI.visible = true
35 | current_preview = file
36 | elif type == 'exemplar': # exemplar
37 | pass
38 | elif type == 'LTEXT': # LTEXT
39 | var file = Core.subfile(type_id, group_id, instance_id, LTEXTSubfile)
40 | $Text.text = file.text
41 | $Text.visible = true
42 | elif type == 'PNG':
43 | var file = Core.subfile(type_id, group_id, instance_id, ImageSubfile)
44 | $Image.texture = file.get_as_texture()
45 | $Image.visible = true
46 | else:
47 | $NoPreview.visible = true
48 | $NoPreview.text = "No preview available for this file (%08x, %08x, %08x)" % [type_id, group_id, instance_id]
49 |
50 | func clear_preview():
51 | $Text.visible = false
52 | $NoPreview.visible = false
53 | $UI.visible = false
54 | $Image.visible = false
55 | # Hide the last previewed UI file
56 | if current_preview != null and (current_preview.index.group_id == 0x96A006B0 or current_preview.index.group_id == 0x08000600):
57 | current_preview.root.visible = false
58 |
59 |
--------------------------------------------------------------------------------
/DATExplorer/SubfilePreview.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=2]
2 |
3 | [ext_resource path="res://SubfilePreview.gd" type="Script" id=2]
4 |
5 | [node name="SubfilePreview" type="PanelContainer"]
6 | anchor_right = 1.0
7 | anchor_bottom = 1.0
8 | script = ExtResource( 2 )
9 |
10 | [node name="Text" type="TextEdit" parent="."]
11 | visible = false
12 | margin_left = 7.0
13 | margin_top = 7.0
14 | margin_right = 1273.0
15 | margin_bottom = 713.0
16 | rect_min_size = Vector2( 500, 200 )
17 | text = "Text preview"
18 | readonly = true
19 | highlight_current_line = true
20 | draw_tabs = true
21 | draw_spaces = true
22 | context_menu_enabled = false
23 | shortcut_keys_enabled = false
24 | virtual_keyboard_enabled = false
25 | middle_mouse_paste_enabled = false
26 | deselect_on_focus_loss_enabled = false
27 | drag_and_drop_selection_enabled = false
28 | caret_block_mode = true
29 |
30 | [node name="UI" type="Control" parent="."]
31 | visible = false
32 | margin_left = 7.0
33 | margin_top = 7.0
34 | margin_right = 1273.0
35 | margin_bottom = 713.0
36 |
37 | [node name="NoPreview" type="Label" parent="."]
38 | visible = false
39 | margin_left = 7.0
40 | margin_top = 353.0
41 | margin_right = 1273.0
42 | margin_bottom = 367.0
43 | text = "No preview available"
44 | align = 1
45 | valign = 1
46 |
47 | [node name="Image" type="TextureRect" parent="."]
48 | margin_left = 7.0
49 | margin_top = 7.0
50 | margin_right = 1273.0
51 | margin_bottom = 713.0
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenSC4
2 | > Open source reimplementation of SimCity 4.
3 |
4 | [](https://discord.gg/53yN9BjA54)
5 |
6 | This project aims to create a working open source "clone" of
7 | SimCity 4 that would be able to take advantage of modern hardware
8 | and run on other platforms.
9 |
10 | # Setup
11 | 1. Install Godot
12 | 2. Git clone and import the project into Godot
13 | 3. Run
14 |
15 | After that, the game will prompt for your SimCity 4 installation
16 | directory and load into a region view.
17 |
18 | # Contributing
19 | We need help on every front for this recreation.
20 | So offering any kind of contribution would be appreciated!
21 |
22 | **Thanks to everyone below who's contributed:**
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Radio/Stations/RadioPlayer.gd:
--------------------------------------------------------------------------------
1 | extends AudioStreamPlayer
2 |
3 | const path_to_radio: String = '/Radio/Stations/Region/Music/%s'
4 |
5 | var current_music
6 | var music_list = []
7 | var rng = RandomNumberGenerator.new()
8 |
9 | func _init():
10 | self.load_music_files()
11 |
12 | func _ready():
13 | self.connect("finished", self, "play_music")
14 |
15 | func play_music():
16 | if len(self.music_list) != 0:
17 | self.current_music = self.music_list[rng.randi_range(0, len(self.music_list)- 1)]
18 | var file = File.new()
19 | file.open(Core.game_dir + path_to_radio % self.current_music, File.READ)
20 |
21 | var audiostream = AudioStreamMP3.new()
22 | audiostream.set_data(file.get_buffer(file.get_len()))
23 | self.set_stream(audiostream)
24 | self.play()
25 |
26 | func load_music_files():
27 | # Load the music list
28 | var dir = Directory.new()
29 | var err = dir.open(Core.game_dir + '/Radio/Stations/Region/Music')
30 | if err != OK:
31 | print('Error opening radio directory: %s' % err)
32 | return
33 | dir.list_dir_begin()
34 | while true:
35 | var file = dir.get_next()
36 | if file == "":
37 | break
38 | if file.ends_with('.mp3'):
39 | self.music_list.append(file)
40 | dir.list_dir_end()
41 |
42 |
--------------------------------------------------------------------------------
/Region.gd:
--------------------------------------------------------------------------------
1 | extends Node2D
2 |
3 | var REGION_NAME = "Timbuktu"
4 | var total_population = 0
5 | var region_w = 0
6 | var region_h = 0
7 | var cities = {}
8 | var radio = []
9 | var current_music
10 | var rng = RandomNumberGenerator.new()
11 | var custom_ui_classes = {
12 | # Main region UI
13 | "0x098f4f6c":preload("res://RegionUI/DisplaySettingsButton.gd"),
14 | "0x09ebe9ee":preload("res://RegionUI/NameAndPopulation.gd"),
15 | "0x09ebee45":preload("res://RegionUI/RegionSubmenu.gd"),
16 | "0x09ebee60":preload("res://RegionUI/TopBarSettingsMenu.gd"),
17 | "0x09ebf2bd":preload("res://RegionUI/RegionManagementButton.gd"),
18 | "0x09ebf2c3":preload("res://RegionUI/TopBarSettingsButton.gd"),
19 | "0x0a5510a9":preload("res://RegionUI/GameSettingsButton.gd"),
20 | "0x26c10a3e":preload("res://RegionUI/ExitGameButton.gd"),
21 | "0x2a5b0000":preload("res://RegionUI/NewRegionButton.gd"),
22 | "0x2a5b0001":preload("res://RegionUI/BrowseRegionsButton.gd"),
23 | "0x2a5b0002":preload("res://RegionUI/DeleteRegionButton.gd"),
24 | "0x2ba290c1":preload("res://RegionUI/ViewOptionsContainer.gd"),
25 | "0x4a779a1a":preload("res://RegionUI/InternetButton.gd"),
26 | "0x6a91dc14":preload("res://RegionUI/TopBarDecoration.gd"),
27 | "0x6a91dc15":preload("res://RegionUI/TopBarSettingsButtonContainer.gd"),
28 | "0x6a91dc16":preload("res://RegionUI/TopBarButtons.gd"),
29 | "0x8a1da655":preload("res://RegionUI/SaveScreenshotButton.gd"),
30 | "0xa98f4f88":preload("res://RegionUI/AudioSettingsButton.gd"),
31 | "0xaba290e1":preload("res://RegionUI/SatelliteViewRadioButton.gd"),
32 | "0xc9e41918":preload("res://RegionUI/PopulationIndicator.gd"),
33 | "0xca1da670":preload("res://RegionUI/BrowseScreenshotsButton.gd"),
34 | "0xca5cfee2":preload("res://RegionUI/ShowNamesCheckbox.gd"),
35 | "0xcba290ec":preload("res://RegionUI/TransportationViewRadioButton.gd"),
36 | "0xea5a96e6":preload("res://RegionUI/ShowBordersCheckbox.gd"),
37 | "0xea5bd179":preload("res://RegionUI/RegionNameDisplay.gd"),
38 | "0xea8cad19":preload("res://RegionUI/Compass.gd"),
39 | # Region prompts
40 | }
41 |
42 | func _init():
43 | Logger.info("Initializing the region view")
44 |
45 | # Open the region INI file
46 | #var _ini = INISubfile.new("res://Regions/%s/region.ini" % REGION_NAME)
47 |
48 |
49 | func anchror_sort(a, b):
50 | if a[0] != b[0]: # non draw
51 | return a[0] < b[0]
52 | else: # bigger tile first
53 | return a[2] > b[2]
54 |
55 | func _ready():
56 | Logger.info("Region node is ready")
57 | var total_pop = 0
58 | for city in self.get_children():
59 | if city is RegionCityView:
60 | city.display()
61 | total_pop += city.get_total_pop()
62 | Logger.info("Total population: %d" % [total_pop])
63 | # Count the city files in the region folder
64 | # City files end in .sc4
65 | var files = []
66 | var dir = Directory.new()
67 | var region_dir_full_path = Core.get_gamedata_path('Regions/%s/' % REGION_NAME)
68 | var err = dir.open(region_dir_full_path)
69 | if err != OK:
70 | Logger.error('Error opening region directory \'%s\': %s' % [region_dir_full_path, err])
71 | return
72 | dir.list_dir_begin()
73 | while true:
74 | var file = dir.get_next()
75 | if file == "":
76 | break
77 | if file.ends_with('.sc4'):
78 | files.append(file)
79 | dir.list_dir_end()
80 | self.read_config_bmp()
81 | var anchor = []
82 | for f in files:
83 | var city = load("res://RegionUI/RegionCityView.tscn").instance()
84 | city.init(Core.get_gamedata_path('Regions/%s/%s' % [REGION_NAME, f]))
85 | var x : int = city.city_info.location[0]
86 | var y : int = city.city_info.location[1]
87 | var width : int = city.city_info.size[0]
88 | var height : int = city.city_info.size[1]
89 | self.total_population += city.city_info.population_residential
90 | var vert_comp = (x+width) + (y+height) - width
91 | anchor.append([vert_comp, city, width])
92 | anchor.sort_custom(self, "anchror_sort")
93 |
94 | for anch in anchor:
95 | var city = anch[1]
96 | var x : int = city.city_info.location[0]
97 | var y : int = city.city_info.location[1]
98 | var width : int = city.city_info.size[0]
99 | var height : int = city.city_info.size[1]
100 | for i in range(x, x+width):
101 | for j in range(y, y+height):
102 | $BaseGrid.cities[i][j] = city
103 | $BaseGrid.add_child(city)
104 | $RadioPlayer.play_music()
105 | load_ui()
106 |
107 | func read_config_bmp():
108 | var region_config_file = File.new()
109 | region_config_file.open(Core.get_gamedata_path("Regions/%s/config.bmp" % REGION_NAME), File.READ)
110 | var data = region_config_file.get_buffer(region_config_file.get_len())
111 | var region_config = Image.new()
112 | region_config.load_bmp_from_buffer(data)
113 |
114 | # Iterate over the pixels
115 | $BaseGrid.init_cities_array(region_config.get_width(), region_config.get_height())
116 | region_w = region_config.get_width()
117 | region_h = region_config.get_height()
118 | region_config.lock()
119 | for i in range(region_config.get_width()):
120 | for j in range(region_config.get_height()):
121 | # Get the pixel at i,j
122 | var pixel = region_config.get_pixel(i, j)
123 | if pixel[0] == 1: # small tile
124 | self.cities[[i, j]] = true
125 | elif pixel[1] == 1: # medium tile
126 | self.cities[[i, j]] = true
127 | for k in range(2):
128 | for l in range(2):
129 | region_config.set_pixel(i + k, j + l, Color(0, 0, 0, 0))
130 | elif pixel[2] == 1: # large tile
131 | self.cities[[i, j]] = true
132 | for k in range(4):
133 | for l in range(4):
134 | if k == 0 and l == 0:
135 | continue
136 | region_config.set_pixel(i + k, j + l, Color(0, 0, 0, 0))
137 |
138 |
139 | func close_all_prompts():
140 | for city in $BaseGrid.get_children():
141 | if city is RegionCityView:
142 | city.visible = true
143 | var prompt = city.get_node_or_null("UnincorporatedCityPrompt")
144 | if prompt != null:
145 | prompt.queue_free()
146 |
147 | func _DEBUG_extract_files(type_id, group_id):
148 | var list_of_instances = Core.get_list_instances(type_id, group_id)
149 | if type_id == "PNG":
150 | for item in list_of_instances:
151 | # Filter bad numbers, maybe holes? I don't know
152 | if item in [1269886195,339829152, 339829153,
153 | 339829154, 339829155, 1809881377,
154 | 1809881378, 1809881379, 1809881380,
155 | 1809881381, 1809881382, 3929989376,
156 | 3929989392, 3929989408, 3929989424,
157 | 3929989440, 3929989456, 338779648,
158 | 338779664, 338779680, 338779696,
159 | 338779712, 338779728, 338779729,
160 | 733031711, 3413654842]:
161 | continue
162 | var subfile = Core.get_subfile(type_id, group_id, item)
163 | var img = subfile.get_as_texture().get_data()
164 | var path = "user://%s/%s/%s.png" % [type_id, group_id, item]
165 | #var path = "user://UI/%s.png" % [item]
166 | img.save_png(path)
167 | else:
168 | Logger.wanr("Type: %s is not yet implemented." % type_id)
169 |
170 | func build_button(button, instance_id):
171 | var btn_img = Core.get_subfile("PNG", "UI_IMAGE", instance_id)
172 | button.texture_disabled = AtlasTexture.new()
173 | button.texture_disabled.atlas = btn_img.get_as_texture()
174 | button.texture_disabled.region = Rect2(0, 0, 80 ,60)
175 |
176 | button.texture_normal = AtlasTexture.new()
177 | button.texture_normal.atlas = btn_img.get_as_texture()
178 | button.texture_normal.region = Rect2(80, 0, 80 ,60)
179 |
180 | button.texture_pressed = AtlasTexture.new()
181 | button.texture_pressed.atlas = btn_img.get_as_texture()
182 | button.texture_pressed.region = Rect2(160, 0, 80 ,60)
183 |
184 | button.texture_hover = AtlasTexture.new()
185 | button.texture_hover.atlas = btn_img.get_as_texture()
186 | button.texture_hover.region = Rect2(240, 0, 80 ,60)
187 |
188 |
189 | func build_top_buttons():
190 | build_button($UICanvas.get_child(2).get_child(1).get_child(0), 339829505)
191 | build_button($UICanvas.get_child(2).get_child(1).get_child(1), 339829506)
192 | build_button($UICanvas.get_child(2).get_child(1).get_child(2), 339829507)
193 |
194 | func load_ui():
195 | preload("res://addons/dbpf/GZWinBtn.gd")
196 | var ui = Core.subfile(0x0, 0x96a006b0, 0xaa920991, SC4UISubfile)
197 | ui.add_to_tree($UICanvas, self.custom_ui_classes)
198 |
--------------------------------------------------------------------------------
/Region.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=4 format=2]
2 |
3 | [ext_resource path="res://Region.gd" type="Script" id=2]
4 | [ext_resource path="res://RegionGrid.gd" type="Script" id=3]
5 | [ext_resource path="res://CameraAnchor.gd" type="Script" id=4]
6 | [ext_resource path="res://Radio/Stations/RadioPlayer.gd" type="Script" id=5]
7 |
8 | [sub_resource type="CanvasItemMaterial" id=1]
9 |
10 | [sub_resource type="ImageTexture" id=2]
11 |
12 | [node name="Region" type="Node2D"]
13 | script = ExtResource( 2 )
14 |
15 | [node name="BaseGrid" type="TileMap" parent="."]
16 | position = Vector2( 1, 1 )
17 | mode = 2
18 | cell_quadrant_size = 64
19 | cell_custom_transform = Transform2D( 90, 18.7, -37.3, 45, 0, 0 )
20 | cell_tile_origin = 2
21 | format = 1
22 | script = ExtResource( 3 )
23 |
24 | [node name="CameraAnchor" type="KinematicBody2D" parent="."]
25 | script = ExtResource( 4 )
26 |
27 | [node name="MainCamera" type="Camera2D" parent="CameraAnchor"]
28 | anchor_mode = 0
29 | current = true
30 | drag_margin_left = 0.8
31 | drag_margin_top = 0.8
32 | drag_margin_right = 0.8
33 | drag_margin_bottom = 0.8
34 |
35 | [node name="RadioPlayer" type="AudioStreamPlayer" parent="."]
36 | script = ExtResource( 5 )
37 |
38 | [node name="ParallaxBackground" type="ParallaxBackground" parent="."]
39 |
40 | [node name="ParallaxLayer" type="ParallaxLayer" parent="ParallaxBackground"]
41 | motion_mirroring = Vector2( 128, 128 )
42 |
43 | [node name="Grid" type="TextureRect" parent="ParallaxBackground/ParallaxLayer"]
44 | margin_right = 5433.0
45 | margin_bottom = 3155.0
46 | mouse_filter = 2
47 | stretch_mode = 2
48 |
49 | [node name="UICanvas" type="CanvasLayer" parent="."]
50 |
--------------------------------------------------------------------------------
/RegionGrid.gd:
--------------------------------------------------------------------------------
1 | extends TileMap
2 |
3 |
4 | var cities : Array = []
5 | var width : int = 0
6 | var height : int = 0
7 |
8 | func init_cities_array(width_, height_):
9 | self.width = width_
10 | self.height = height_
11 | for i in range(width_):
12 | cities.append([])
13 | for _j in range(height_):
14 | cities[i].append(null)
15 |
16 | func _unhandled_input(event):
17 | if event is InputEventMouseButton and event.doubleclick:
18 | # Get the grid position
19 | var grid_position : Vector2 = world_to_map(get_global_mouse_position())
20 | if grid_position.x >= 0 and grid_position.x < width and grid_position.y >= 1 and grid_position.y < height:
21 | cities[grid_position.x][grid_position.y].open_city()
22 |
23 |
--------------------------------------------------------------------------------
/RegionUI/AudioSettingsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "AudioSettingsButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/BrowseRegionsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "BrowseRegionsButton"
--------------------------------------------------------------------------------
/RegionUI/BrowseScreenshotsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "BrowseScreenshotsButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/CityThumbnailArea.gd:
--------------------------------------------------------------------------------
1 | extends Area2D
2 |
3 |
4 | func _ready():
5 | pass
6 |
--------------------------------------------------------------------------------
/RegionUI/Compass.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_TOP_LEFT, true)
5 | self.name="Compass"
--------------------------------------------------------------------------------
/RegionUI/DeleteRegionButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "DeleteRegionButton"
--------------------------------------------------------------------------------
/RegionUI/DisplaySettingsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "DisplaySettingsButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/ExitGameButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "ExitGameButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/GameSettingsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "GameSettingsButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/InternetButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "InternetButton"
5 | self.connect("clicked", self, "_on_clicked")
6 |
7 | func _on_clicked():
8 | OS.shell_open("https://github.com/OpenSC4-org/OpenSC4")
9 |
--------------------------------------------------------------------------------
/RegionUI/NameAndPopulation.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_BOTTOM_LEFT, true)
5 | self.name="NameAndPopulation"
--------------------------------------------------------------------------------
/RegionUI/NewRegionButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "NewRegionButton"
5 |
--------------------------------------------------------------------------------
/RegionUI/PopulationIndicator.gd:
--------------------------------------------------------------------------------
1 | extends GZWinText
2 |
3 | func format_thousands(val):
4 | var res = "%d" % floor(val / 1000)
5 | var separator = " "
6 | while val >= 1000:
7 | res = "%s%s%03d" % [res, separator, (val % 1000)]
8 | val /= 1000
9 | return res
10 |
11 | func _init(attributes).(attributes):
12 | self.name = "RegionNameDisplay"
13 |
14 | func _ready():
15 | var population = get_node("/root/Region").total_population
16 | self.set_text(format_thousands(population))
17 |
--------------------------------------------------------------------------------
/RegionUI/RegionCityView.gd:
--------------------------------------------------------------------------------
1 | extends Area2D
2 | class_name RegionCityView
3 |
4 | var region_view_thumbnails : Array = []
5 | var savefile : DBPF
6 | var city_info : SC4ReadRegionalCity
7 | var city_name : String = ""
8 |
9 | # Size, in pixels, of the tile base
10 |
11 | var TILE_BASE_HEIGHT = 18
12 | var TILE_BASE_WIDTH = 90
13 |
14 |
15 | func init(filepath : String):
16 | savefile = DBPF.new(filepath)
17 | # Load the thumbnails
18 | # Note: should be 0, 2, 4, 6, but for some reason only 2 and 4 are ever present
19 | for instance_id in [0, 2]:
20 | region_view_thumbnails.append(savefile.get_subfile(0x8a2482b9, 0x4a2482bb, instance_id, ImageSubfile).get_as_texture())
21 | city_info = savefile.get_subfile(0xca027edb, 0xca027ee1, 0, SC4ReadRegionalCity)
22 | self.city_name = filepath.get_file().get_basename()
23 | if self.city_name.find("City - ") == 0:
24 | self.city_name = self.city_name.substr(7)
25 |
26 | func _ready():
27 | display()
28 |
29 | func display(): # TODO city edges override other cities causing glitches, can be solved by controlling the draw order or by adding a z value
30 | if city_info.mode == 'god' or not Core.region_settings.get("show_names", true):
31 | $InfoContainer/CityName.visible = false
32 | else:
33 | $InfoContainer/CityName.text = self.city_name
34 | # Print city size
35 | var pos_on_grid = get_parent().map_to_world(Vector2(city_info.location[0], city_info.location[1]))
36 | #var thumbnail_texture : Texture = region_view_thumbnails[0]
37 | # The height of a tile if it were completely flat
38 | #print(region_view_thumbnails[0].get_data().data["height"], region_view_thumbnails[0].get_data().data["width"], "\t", region_view_thumbnails[1].get_data().data["height"], region_view_thumbnails[1].get_data().data["width"])
39 | var mystery_img = region_view_thumbnails[1].get_data()
40 | var region_img = region_view_thumbnails[0].get_data()
41 | mystery_img.lock()
42 | region_img.lock()
43 | var min_h = mystery_img.data["height"]
44 | var min_w = mystery_img.data["width"]
45 | for w in range(mystery_img.data["width"]):
46 | for h in range(mystery_img.data["height"]):
47 | var m_pix = mystery_img.get_pixel(w, h)
48 | if (m_pix.b) > 0.75:
49 | if h < min_h: min_h = h
50 | if w < min_w: min_w = w
51 | var border = Color(0.0, 0.0, 0.0, 0.0)
52 | if Core.region_settings.get("show_borders", true):
53 | border = Color(m_pix.g/10, m_pix.g/10, m_pix.g/10, 0.0)
54 | var r_pix = (Color(m_pix.b, m_pix.b, m_pix.b, 1.0) * region_img.get_pixel(w, h)) + border
55 | region_img.set_pixel(w, h, r_pix)
56 | else:
57 | region_img.set_pixel(w, h, Color(0.0, 0.0, 0.0, 0.0))
58 | #var trim = Rect2(Vector2(float(min_w), float(min_h)), Vector2(mystery_img.data["width"], mystery_img.data["height"]))
59 | #var trimmed = region_img.get_rect(trim)
60 | var thumbnail_texture = ImageTexture.new()
61 | thumbnail_texture.create_from_image(region_img, 0)
62 | var expected_height = 63.604 * city_info.size[1]
63 | # Adjust the tile placement
64 | var extra_height = thumbnail_texture.get_height() - expected_height
65 | pos_on_grid.y -= extra_height
66 | pos_on_grid.x -= 37.305 * city_info.size[1]
67 | self.translate(pos_on_grid)
68 | $Thumbnail.texture = thumbnail_texture
69 | $CollisionShape.shape.extents = Vector2(thumbnail_texture.get_width() / 2, thumbnail_texture.get_height() / 2)
70 | $InfoContainer/CityName.set_position(Vector2(thumbnail_texture.get_width() / 2, expected_height / 2))
71 |
72 | func get_total_pop():
73 | return city_info.population_residential
74 |
75 | func save_thumbnail():
76 | region_view_thumbnails[0].get_data().save_png("region_view_thumbnail.png")
77 |
78 | func open_city():
79 | Boot.current_city = savefile
80 | var err = get_tree().change_scene("res://CityView/CityScene/City.tscn")
81 | if err != OK:
82 | print("Error trying to change the scene to the city")
83 |
--------------------------------------------------------------------------------
/RegionUI/RegionCityView.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=5 format=2]
2 |
3 | [ext_resource path="res://RegionUI/RegionCityView.gd" type="Script" id=1]
4 |
5 | [sub_resource type="RectangleShape2D" id=1]
6 |
7 | [sub_resource type="Shader" id=2]
8 | code = "shader_type canvas_item;
9 |
10 | void fragment(){
11 | COLOR = texture(TEXTURE, UV);
12 | if (COLOR.a < 1.0){
13 | COLOR.a = 0.0;
14 | }
15 | }"
16 |
17 | [sub_resource type="ShaderMaterial" id=3]
18 | shader = SubResource( 2 )
19 |
20 | [node name="RegionCityView" type="Area2D"]
21 | script = ExtResource( 1 )
22 |
23 | [node name="CollisionShape" type="CollisionShape2D" parent="."]
24 | shape = SubResource( 1 )
25 |
26 | [node name="Thumbnail" type="Sprite" parent="."]
27 | material = SubResource( 3 )
28 | centered = false
29 |
30 | [node name="InfoContainer" type="CenterContainer" parent="."]
31 | anchor_right = 1.0
32 | margin_right = 40.0
33 | margin_bottom = 40.0
34 | size_flags_horizontal = 3
35 | size_flags_vertical = 3
36 |
37 | [node name="CityName" type="Label" parent="InfoContainer"]
38 | margin_left = 6.0
39 | margin_top = 4.0
40 | margin_right = 33.0
41 | margin_bottom = 35.0
42 | size_flags_horizontal = 6
43 | size_flags_vertical = 6
44 | text = "CITY
45 | "
46 | align = 1
47 | valign = 1
48 |
--------------------------------------------------------------------------------
/RegionUI/RegionManagementButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "RegionManagementButton"
5 | self.connect("toggled_on", self, "_on_toggled_on")
6 | self.connect("toggled_off", self, "_on_toggled_off")
7 |
8 |
9 | #TODO: Use notifications instead of signals for greater flexibility?
10 | func _on_toggled_on():
11 | $"../../RegionSubmenu".set_visible(true)
12 |
13 | func _on_toggled_off():
14 | $"../../RegionSubmenu".set_visible(false)
15 |
--------------------------------------------------------------------------------
/RegionUI/RegionNameDisplay.gd:
--------------------------------------------------------------------------------
1 | extends GZWinText
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "RegionNameDisplay"
5 |
6 | func _ready():
7 | self.set_text(get_node("/root/Region").REGION_NAME)
8 |
--------------------------------------------------------------------------------
/RegionUI/RegionSubmenu.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_CENTER_TOP, true)
5 | self.name="RegionSubmenu"
6 | self.visible = false
--------------------------------------------------------------------------------
/RegionUI/SatelliteViewRadioButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "SatelliteViewRadioButton"
--------------------------------------------------------------------------------
/RegionUI/SaveScreenshotButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "SaveScreenshotButton"
--------------------------------------------------------------------------------
/RegionUI/ShowBordersCheckbox.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "ShowBordersCheckbox"
--------------------------------------------------------------------------------
/RegionUI/ShowNamesCheckbox.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes).(attributes):
4 | self.name = "ShowNamesCheckbox"
5 |
--------------------------------------------------------------------------------
/RegionUI/Thumbnail.gd:
--------------------------------------------------------------------------------
1 | extends TextureRect
2 |
3 |
4 | func _ready():
5 | pass
6 |
--------------------------------------------------------------------------------
/RegionUI/TopBarButtons.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_CENTER_TOP, true)
5 | self.name="TopBarButtons"
--------------------------------------------------------------------------------
/RegionUI/TopBarDecoration.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_CENTER_TOP, true)
5 | self.name = "TopBarDecoration"
6 |
--------------------------------------------------------------------------------
/RegionUI/TopBarSettingsButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name="TopBarSettingsButton"
5 | self.connect("toggled_on", self, "_on_toggled_on")
6 | self.connect("toggled_off", self, "_on_toggled_off")
7 |
8 | func _on_toggled_on():
9 | $"../../TopBarSettingsMenu".set_visible(true)
10 |
11 | func _on_toggled_off():
12 | $"../../TopBarSettingsMenu".set_visible(false)
--------------------------------------------------------------------------------
/RegionUI/TopBarSettingsButtonContainer.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_TOP_RIGHT, true)
5 | self.name = "TopBarSettingsButtonContainer"
--------------------------------------------------------------------------------
/RegionUI/TopBarSettingsMenu.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.set_anchors_preset(PRESET_TOP_RIGHT, true)
5 | self.name="TopBarSettingsMenu"
6 | self.visible = false
--------------------------------------------------------------------------------
/RegionUI/TransportationViewRadioButton.gd:
--------------------------------------------------------------------------------
1 | extends GZWinBtn
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | self.name = "TransportationViewRadioButton"
--------------------------------------------------------------------------------
/RegionUI/ViewOptionsContainer.gd:
--------------------------------------------------------------------------------
1 | extends GZWinGen
2 |
3 | func _init(attributes : Dictionary).(attributes):
4 | pass
5 |
--------------------------------------------------------------------------------
/RegionViewCityThumbnail.gd:
--------------------------------------------------------------------------------
1 | extends Sprite
2 |
3 | class_name RegionViewCityThumbnail
4 |
5 | func _unhandled_input(event):
6 | if event is InputEventMouseButton and not event.is_echo() and event.button_index == BUTTON_LEFT:
7 | var local_pos = to_local(event.global_position)
8 | if get_rect().has_point(local_pos):
9 | print('Click at %d %d' % [local_pos.x, local_pos.y])
10 | print('Rect: %d %d %d %d' % [get_rect().position.x, get_rect().position.y, get_rect().size.x, get_rect().size.y])
11 | get_tree().set_input_as_handled()
12 |
--------------------------------------------------------------------------------
/SC4City__WriteRegionViewThumbnail.gd:
--------------------------------------------------------------------------------
1 | extends "res://addons/dbpf/SpriteSubfile.gd"
2 |
3 | func _init(index).(index):
4 | pass
5 |
--------------------------------------------------------------------------------
/SC4ReadRegionalCity.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 | class_name SC4ReadRegionalCity
3 |
4 | var version = [0,0]
5 | var location = [0,0]
6 | var size = [0,0]
7 | var population_residential : int = 0
8 | var population_commercial : int = 0
9 | var population_industrial : int = 0
10 | var mayor_rating : int = 0
11 | var star_count : int = 0
12 | var unknown = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
13 | var tutorial_flag : bool = false
14 | var guid : int = 0
15 | var mode : String = "god"
16 |
17 | func _init(index).(index):
18 | pass
19 |
20 | func load(file, dbdf=null):
21 | .load(file, dbdf)
22 | var stream = StreamPeerBuffer.new()
23 | stream.data_array = raw_data
24 | self.version = [stream.get_16(), stream.get_16()];
25 | self.location = [stream.get_32(), stream.get_32()];
26 | self.size = [stream.get_32(), stream.get_32()];
27 | self.population_residential = stream.get_32();
28 | self.population_commercial = stream.get_32();
29 | self.population_industrial = stream.get_32();
30 | stream.get_float();
31 | self.mayor_rating = stream.get_8()
32 | self.star_count = stream.get_8()
33 | self.tutorial_flag = stream.get_8() == 1
34 | self.guid = stream.get_32()
35 | self.unknown[5] = stream.get_32()
36 | self.unknown[6] = stream.get_32()
37 | self.unknown[7] = stream.get_32()
38 | self.unknown[8] = stream.get_32()
39 | self.unknown[9] = stream.get_32()
40 | var v = stream.get_8()
41 | if v == 0:
42 | self.mode = "god"
43 | elif v == 1:
44 | self.mode = "normal"
45 |
46 |
--------------------------------------------------------------------------------
/SC4UISubfile.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends DBPFSubfile
18 |
19 | class_name SC4UISubfile
20 |
21 | var root : Control = Control.new()
22 | var rectRegex : RegEx = RegEx.new()
23 | var colorRegex : RegEx = RegEx.new()
24 | var imgGIRegex : RegEx = RegEx.new()
25 | var vec2Regex : RegEx = RegEx.new()
26 | var elementsByID : Dictionary = {}
27 | var lines : Array = []
28 |
29 | func _init(index).(index):
30 | rectRegex.compile("\\((?-?\\d+),(?-?\\d+),(?-?\\d+),(?-?\\d+)\\)")
31 | colorRegex.compile("\\((?\\d+),(?\\d+),(?\\d+)\\)")
32 | imgGIRegex.compile("\\{(?[0-9a-fA-F]{8}),(?[0-9a-fA-F]{8})\\}")
33 | vec2Regex.compile("\\((?-?\\d+),(?-?\\d+)\\)")
34 |
35 | func string2color(string : String) -> Color:
36 | var result = colorRegex.search(string)
37 | return Color8(int(result.get_string('r')), int(result.get_string('g')), int(result.get_string('b')))
38 |
39 | func string2rect(string : String) -> Rect2:
40 | var result = rectRegex.search(string)
41 | return Rect2(int(result.get_string('x')), int(result.get_string('y')), int(result.get_string('width')), int(result.get_string('height')))
42 |
43 | func string2intlist(string : String) -> Array:
44 | var parts = string.lstrip("(").rstrip(")").split(",")
45 | var result = Array()
46 | for part in parts:
47 | result.append(int(part))
48 | return result
49 |
50 | func string2vec2(string : String) -> Vector2:
51 | var result = vec2Regex.search(string)
52 | return Vector2(int(result.get_string('x')), int(result.get_string('y')))
53 |
54 | func create_last_element(parts : Array, custom_classes : Dictionary) -> Control:
55 | var attributes = {}
56 | for i in range(1, len(parts)):
57 | var attr = parts[i].split("=")
58 | if len(attr) == 2:
59 | attributes[attr[0]] = attr[1]
60 | return create_element(attributes, custom_classes)
61 |
62 |
63 | func load(file, dbdf=null):
64 | .load(file, dbdf)
65 | lines = stream.get_string(stream.get_available_bytes()).split("\n")
66 |
67 | func add_to_tree(parent : Node, custom_classes : Dictionary):
68 | parent.add_child(self.root)
69 | root.set_anchor(MARGIN_LEFT, 0)
70 | root.set_anchor(MARGIN_TOP, 0)
71 | root.set_anchor(MARGIN_RIGHT, 1)
72 | root.set_anchor(MARGIN_BOTTOM, 1)
73 | var current_element : Control = self.root
74 | var last_element : Control = null
75 | # We add children to current_element
76 | # last_element is the last element we've created
77 | for l in lines:
78 | if l.begins_with("#"):
79 | continue
80 | else:
81 | var parts = l.strip_edges().rstrip(">").lstrip("<").split(" ")
82 | var tag_name = parts[0]
83 | if tag_name == 'LEGACY': # Create a new element
84 | last_element = create_last_element(parts, custom_classes)
85 | # If current_element is null, then this is the root
86 | if current_element != null:
87 | current_element.add_child(last_element)
88 | # hierarchy navigation
89 | elif tag_name == 'CHILDREN':
90 | current_element = last_element
91 | elif tag_name == '/CHILDREN':
92 | current_element = current_element.get_parent()
93 | root.name = 'SC4 UI root'
94 |
95 | func create_element(attributes : Dictionary, custom_classes : Dictionary) -> Control:
96 | var type = attributes.get('iid', 'none')
97 | var interpreted_attributes = interpret_attributes(attributes)
98 | var element = null
99 | # Check if this is a standard class without any particular behaviour,
100 | # or a class that's linked to in-game code
101 | #print(interpreted_attributes)
102 | var class_id = attributes.get('clsid', 'none')
103 | if attributes.get('id', '') in custom_classes: # magic number for a custom class?
104 | var custom_class_id = attributes.get('id', 0)
105 | element = custom_classes[custom_class_id].new(interpreted_attributes)
106 | # else, standard class, instance with the GZcom implementation
107 | elif type == 'IGZWinGen':
108 | element = GZWinGen.new(interpreted_attributes)
109 | elif type == 'IGZWinText':
110 | element = GZWinText.new(interpreted_attributes)
111 | elif type == 'IGZWinBtn':
112 | element = GZWinBtn.new(interpreted_attributes)
113 | elif type == 'IGZWinBMP':
114 | element = GZWinBMP.new(interpreted_attributes)
115 | elif type == 'IGZWinFlatRect':
116 | element = GZWinFlatRect.new(interpreted_attributes)
117 | elif type == 'IGZWinTextEdit':
118 | element = TextEdit.new()
119 | elif type == 'IGZWinCustom':
120 | element = Control.new()
121 | elif type == 'IGZWinGrid':
122 | element = GridContainer.new()
123 | elif type == 'IGZWinSlider':
124 | element = HSlider.new()
125 | elif type == 'IGZWinCombo':
126 | element = CheckBox.new()
127 | elif type == 'IGZWinListBox':
128 | element = ScrollContainer.new()
129 | elif type == 'IGZWinTreeView':
130 | element = Tree.new()
131 | elif type == 'IGZWinScrollbar2':
132 | element = HSlider.new()
133 | elif type == 'IGZWinFolders':
134 | element = FileDialog.new()
135 | elif type == 'IGZWinLineINput':
136 | element = LineEdit.new()
137 | elif type == 'IGZWinFileBrowser':
138 | element = FileDialog.new()
139 | else:
140 | print("Unknown element type %s" % type)
141 | element = GZWinGen.new(interpreted_attributes)
142 | if element.name == '':
143 | if 'id' in interpreted_attributes:
144 | element.name = "%s-%s-%s" % [interpreted_attributes['clsid'], interpreted_attributes['id'], type]
145 | else:
146 | element.name = type
147 | if 'id' in attributes and not attributes['id'] in custom_classes:
148 | print("Missing custom class for id ", attributes['id'])
149 |
150 | return element
151 |
152 | func interpret_attributes(attributes : Dictionary):
153 | var interpreted_attributes : Dictionary = {}
154 | for attr in attributes:
155 | if attr in ['area', 'imagerect', 'vscrollimagerect', 'hscrollimagerect']:
156 | interpreted_attributes[attr] = string2rect(attributes[attr])
157 | # TODO: get all color-type attributes from the Wiki
158 | elif attr in ['color', 'fillcolor', 'forecolor', 'backcolor', 'bkgcolor',
159 | 'colorfont?', 'highlightcolor', 'outlinecolor', 'caretcolor',
160 | 'olinecolor', 'colgridclr', 'rowgridclr', 'coloroutlineb',
161 | 'coloroutliner', 'coloroutlinet', 'coloroutlinel', 'coloroutline',
162 | 'colorright', 'colorbottom', 'colorleft', 'colortop', 'backgroundcolor',
163 | 'highlightcolor', 'backgroundcolor', 'highlightcolorbackground', 'highlightcolorforeground',
164 | 'columngridcolor', 'linegridcolor']:
165 | interpreted_attributes[attr] = string2color(attributes[attr])
166 | elif attr == 'id':
167 | interpreted_attributes[attr] = attributes[attr]
168 | elif attr in ['gutters']:
169 | interpreted_attributes[attr] = string2intlist(attributes[attr])
170 | elif attr.find('winflag_') != -1 or attr in ['edgeimage', 'moveable', 'sizeable', 'defaultkeys',
171 | 'defaultkeys', 'closevisible', 'gobackvisible', 'minmaxvisible']:
172 | interpreted_attributes[attr] = attributes[attr] == 'yes'
173 | elif attr == 'image':
174 | var imgGI = imgGIRegex.search(attributes[attr])
175 | var type_id = 0x856ddbac
176 | # Godot hex_to_int function expects a 0x prefix
177 | var group_id = ("0x%s" % imgGI.get_string('group')).hex_to_int()
178 | var instance_id = ("0x%s" % imgGI.get_string('instance')).hex_to_int()
179 | var image = Core.subfile(type_id, group_id, instance_id, ImageSubfile)
180 | interpreted_attributes[attr] = image
181 | else:
182 | if false:
183 | print("[%s] %s = '%s'" % [attributes['clsid'], attr, attributes[attr]])
184 | interpreted_attributes[attr] = attributes[attr]
185 | return interpreted_attributes
186 |
187 |
--------------------------------------------------------------------------------
/SubfilePreview.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=2]
2 |
3 | [ext_resource path="res://DATExplorer/SubfilePreview.gd" type="Script" id=2]
4 |
5 | [node name="SubfilePreview" type="PanelContainer"]
6 | anchor_right = 1.0
7 | anchor_bottom = 1.0
8 | script = ExtResource( 2 )
9 |
10 | [node name="Text" type="TextEdit" parent="."]
11 | visible = false
12 | margin_left = 7.0
13 | margin_top = 7.0
14 | margin_right = 1273.0
15 | margin_bottom = 713.0
16 | rect_min_size = Vector2( 500, 200 )
17 | text = "Text preview"
18 | readonly = true
19 | highlight_current_line = true
20 | draw_tabs = true
21 | draw_spaces = true
22 | context_menu_enabled = false
23 | shortcut_keys_enabled = false
24 | virtual_keyboard_enabled = false
25 | middle_mouse_paste_enabled = false
26 | deselect_on_focus_loss_enabled = false
27 | drag_and_drop_selection_enabled = false
28 | caret_block_mode = true
29 |
30 | [node name="UI" type="Control" parent="."]
31 | visible = false
32 | margin_left = 7.0
33 | margin_top = 7.0
34 | margin_right = 1273.0
35 | margin_bottom = 713.0
36 |
37 | [node name="NoPreview" type="Label" parent="."]
38 | visible = false
39 | margin_left = 7.0
40 | margin_top = 353.0
41 | margin_right = 1273.0
42 | margin_bottom = 367.0
43 | text = "No preview available"
44 | align = 1
45 | valign = 1
46 |
47 | [node name="Image" type="TextureRect" parent="."]
48 | margin_left = 7.0
49 | margin_top = 7.0
50 | margin_right = 1273.0
51 | margin_bottom = 713.0
52 |
--------------------------------------------------------------------------------
/Terrain.gd:
--------------------------------------------------------------------------------
1 | extends MeshInstance
2 |
3 | var tmpMesh = ArrayMesh.new();
4 | var vertices = PoolVector3Array()
5 | var UVs = PoolVector2Array()
6 | var color = Color(0.9, 0.1, 0.1)
7 | var mat = self.get_material_override()
8 | var st = SurfaceTool.new()
9 | var tm_table
10 |
11 | func _ready():
12 |
13 | vertices.push_back(Vector3(1,0,0))
14 | vertices.push_back(Vector3(1,0,1))
15 | vertices.push_back(Vector3(0,0,1))
16 | vertices.push_back(Vector3(0,0,0))
17 |
18 | UVs.push_back(Vector2(0,0))
19 | UVs.push_back(Vector2(0,1))
20 | UVs.push_back(Vector2(1,1))
21 | UVs.push_back(Vector2(1,0))
22 |
23 | #mat.albedo_color = color
24 | st.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)
25 | for v in vertices.size():
26 | #st.add_color(color)
27 | st.add_uv(UVs[v])
28 | st.add_vertex(vertices[v])
29 |
30 | st.commit(tmpMesh)
31 | self.set_mesh(tmpMesh)
32 |
33 |
34 | func load_textures_to_uv_dict():
35 | var type_ini = 0x00000000
36 | var group_ini = 0x8a5971c5
37 | var instance_ini = 0xAA597172
38 | var file = Core.subfile(type_ini, group_ini, instance_ini, DBPFSubfile)
39 | var ini_str = file.raw_data.get_string_from_ascii()
40 | var ini = {}
41 | var current_section = ''
42 | for line in ini_str.split('\n'):
43 | line = line.strip_edges(true, true)
44 | if line.length() == 0:
45 | continue
46 | if line[0] == '#' or line[0] == ';':
47 | continue
48 | if line[0] == '[':
49 | current_section = line.substr(1, line.length() - 2)
50 | ini[current_section] = {}
51 | else:
52 | var key = line.split('=')[0]
53 | var value = line.split('=')[1]
54 | ini[current_section][key] = value
55 | var textures = [17, 16, 21]
56 | var tm_dict = ini["TropicalTextureMapTable"]
57 | var cliff_index
58 | var beach_index
59 | var top_edge
60 | var mid_edge
61 | var bot_edge
62 | tm_table = []
63 | for line in ini["TropicalMiscTextures"].keys():
64 | if line == "LowCliff":
65 | textures.append((ini["TropicalMiscTextures"][line]).hex_to_int())
66 | cliff_index = (ini["TropicalMiscTextures"][line]).hex_to_int()
67 | elif line == "Beach":
68 | textures.append((ini["TropicalMiscTextures"][line]).hex_to_int())
69 | beach_index = (ini["TropicalMiscTextures"][line]).hex_to_int()
70 | for line in tm_dict.keys():
71 | var line_r = []
72 | for val_i in range(len(tm_dict[line].split(','))):
73 | var hexval = (tm_dict[line].split(',')[val_i])
74 | if hexval == "":
75 | continue
76 | var val = hexval.hex_to_int()
77 | if val != 0: # space at end of line returned 0 value from hex_to_int
78 | line_r.append(val)
79 | if not textures.has(val):
80 | textures.append(val)
81 | tm_table.append(line_r)
82 | var type_tex = 0x7ab50e44
83 | var group_tex = 0x891B0E1A
84 | var img_dict = {}
85 | var width = 0
86 | var height = 0
87 | var formats = []
88 | var uv_dict = {}
89 | var d_len = 0
90 | for instance in textures:
91 | while true: # set array index for textures to be used in shader
92 | var ind = tm_table.find(instance)
93 | if ind == -1:
94 | break
95 | for zoom in range(5):
96 | var inst_z = instance + (zoom * 256)
97 | var fsh_subfile = Core.subfile(
98 | type_tex, group_tex, inst_z, FSHSubfile
99 | )
100 | if not formats.has(fsh_subfile.img.get_format()):
101 | formats.append(fsh_subfile.img.get_format())
102 | var data_len = len(fsh_subfile.img.data["data"])
103 | if data_len > d_len:
104 | d_len = data_len
105 | if data_len == 0:
106 | print("error invalid FSH")
107 | if width < fsh_subfile.width:
108 | width = fsh_subfile.width
109 | height = fsh_subfile.height
110 | img_dict[inst_z] = fsh_subfile
111 | var w_size = ceil(16384 / width)
112 | var h_size = ceil(16384 / height)
113 | var tot_w
114 | var tot_h
115 | var format
116 | if len(formats) == 1:
117 | format = formats[0]
118 | else:
119 | print("TODO need to handle multiple formats")
120 | if len(textures) > w_size:
121 | tot_w = 16384
122 | tot_h = ceil(len(textures) / w_size) * 5 * height
123 | else:
124 | tot_w = len(textures) * width
125 | tot_h = 5 * height
126 | var uv_mipmap_offset = height / tot_h
127 | var arr_data = [PoolByteArray([])]
128 | while len(arr_data) < tot_h:
129 | arr_data.append_array(arr_data)
130 | arr_data = arr_data.slice(0, tot_h)
131 | var format_decomp
132 | var textarr = TextureArray.new()
133 | textarr.create (width, height, len(textures) * 5, formats[0], 2)
134 | var layer = 0
135 | var ind_to_layer = {}
136 | for im_ind in img_dict.keys():
137 | var image = img_dict[im_ind].img
138 | textarr.set_layer_data(image, layer)
139 | if im_ind < 256:
140 | ind_to_layer[im_ind] = layer
141 | if im_ind == cliff_index:
142 | cliff_index = layer
143 | elif im_ind == beach_index:
144 | beach_index = layer
145 | elif im_ind == 17:
146 | top_edge = layer
147 | elif im_ind == 16:
148 | mid_edge = layer
149 | elif im_ind == 21:
150 | bot_edge = layer
151 | var test = textarr.get_layer_data(layer)
152 | if len(test.data["data"]) == 0:
153 | print("failed to load layer", layer, "with image", im_ind)
154 | layer += 1
155 |
156 | self.mat.set_shader_param("cliff_ind", float(cliff_index))
157 | self.mat.set_shader_param("beach_ind", float(beach_index))
158 | self.mat.set_shader_param("terrain", textarr)
159 | self.set_material_override(self.mat)
160 | var mat_e = self.get_parent().get_node("Border").get_material_override()
161 | mat_e.set_shader_param("terrain", textarr)
162 | mat_e.set_shader_param("top_ind", float(top_edge))
163 | mat_e.set_shader_param("mid_ind", float(mid_edge))
164 | mat_e.set_shader_param("bot_ind", float(bot_edge))
165 | return ind_to_layer
166 |
167 |
168 |
169 |
170 |
171 | # Called every frame. 'delta' is the elapsed time since the previous frame.
172 | #func _process(delta):
173 | # pass
174 |
--------------------------------------------------------------------------------
/addons/dbpf/CURSubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 |
3 | class_name CURSubfile
4 |
5 | var n_images
6 | var entries = []
7 |
8 | func _init(index).(index):
9 | pass
10 |
11 | func load(file, dbdf=null):
12 | .load(file, dbdf)
13 | file.seek(index.location)
14 | var ind = 0
15 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
16 | # 4 bytes (char) - signature
17 | var signature = self.get_int_from_bytes(raw_data.subarray(ind+2, ind+3))
18 | assert(signature == 2, "DBPFSubfile.load: not a CUR file")
19 | ind += 4
20 | # 4 bytes (unint32) - total file size
21 | self.n_images = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
22 | ind += 2
23 | for n in range(n_images):
24 | var entry = CUR_Entry.new()
25 | entry.width = raw_data[ind]
26 | ind += 1
27 | entry.height = raw_data[ind]
28 | ind +=3
29 | entry.x_hotspot = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
30 | ind += 2
31 | entry.y_hotspot = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
32 | entry.vec_hotspot = Vector2(entry.x_hotspot, entry.y_hotspot)
33 | ind += 2
34 | entry.size = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
35 | ind += 4
36 | entry.offset = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
37 | ind += 4
38 | entries.append(entry)
39 | for entry in entries:
40 | ind = entry.offset
41 | if (raw_data[ind] == 0x89 and
42 | raw_data[ind+1] == 0x50 and
43 | raw_data[ind+2] == 0x4E and
44 | raw_data[ind+3] == 0x47):
45 | entry.img = Image.new()
46 | var img_data = raw_data.subarray(ind, ind+entry.size)
47 | entry.img.load_png_from_buffer(img_data)
48 | elif self.get_int_from_bytes(raw_data.subarray(ind, ind+3)) == 40:
49 | ind += 4
50 | var bmp_width = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
51 | ind += 4
52 | var bmp_height = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
53 | ind += 6
54 | var bpp = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
55 | ind += 2
56 | #var comp_meth = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
57 | ind += 4
58 | var img_size = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
59 | ind += 12
60 | #var n_colors = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
61 | ind += 8
62 | var bmp_size = (bpp/8) * bmp_width * bmp_height
63 | var bmp_header = [0x42, 0x4d,
64 | (int(bmp_size)+54), (int(bmp_size)+54)>>8, (int(bmp_size)+54)>>16, (int(bmp_size)+54)>>24,
65 | 0x00, 0x00, 0x00, 0x00,
66 | 0x36, 0x00, 0x00, 0x00]
67 | var bmp_data = PoolByteArray(bmp_header)
68 | bmp_data.append_array(raw_data.subarray(ind-40, ind-1))
69 | bmp_data.append_array(raw_data.subarray(ind, ind+bmp_size-1))
70 | #bmp_data[22] = bmp_data[18]
71 | var temp_img = Image.new()
72 | temp_img.load_bmp_from_buffer(bmp_data)
73 | temp_img.decompress()
74 | temp_img = temp_img.get_rect(Rect2(Vector2(0.0, 0.0), Vector2(bmp_width, bmp_width)))
75 | var mask_data = raw_data.subarray(ind+bmp_size, raw_data.size()-1)
76 | for i in range(bmp_width):
77 | for j in range(0, bmp_width, 8):
78 | var k = (j / 8) + (i * 4)
79 | var l = ((bmp_width - (i+1)) * bmp_width + j) * 4
80 | temp_img.data["data"][l+3+28] = 255-min((mask_data[(k)] & 1) * 256, 255)
81 | temp_img.data["data"][l+3+24] = 255-min((mask_data[(k)] & 2) * 128, 255)
82 | temp_img.data["data"][l+3+20] = 255-min((mask_data[(k)] & 4) * 64, 255)
83 | temp_img.data["data"][l+3+16] = 255-min((mask_data[(k)] & 8) * 32, 255)
84 | temp_img.data["data"][l+3+12] = 255-min((mask_data[(k)] & 16) * 16, 255)
85 | temp_img.data["data"][l+3+8] = 255-min((mask_data[(k)] & 32) * 8, 255)
86 | temp_img.data["data"][l+3+4] = 255-min((mask_data[(k)] & 64) * 4, 255)
87 | temp_img.data["data"][l+3] = 255-min((mask_data[(k)] & 128) * 2, 255)
88 | entry.img = temp_img
89 | return OK
90 |
91 |
92 | func get_int_from_bytes(bytearr):
93 | var r_int = 0
94 | var shift = 0
95 | for byte in bytearr:
96 | r_int = (r_int) | (byte << shift)
97 | shift += 8
98 | return r_int
99 |
100 | func get_as_texture(entry_no = 0):
101 | assert(entries[entry_no].img != null)
102 | var ret = ImageTexture.new()
103 | ret.create_from_image(entries[entry_no].img, 0)
104 | return ret
105 |
--------------------------------------------------------------------------------
/addons/dbpf/CUR_Entry.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | class_name CUR_Entry
4 |
5 | var width
6 | var height
7 | var x_hotspot
8 | var y_hotspot
9 | var vec_hotspot
10 | var size
11 | var offset
12 | var img
13 |
14 | func _init():
15 | pass
16 |
--------------------------------------------------------------------------------
/addons/dbpf/DBDF.gd:
--------------------------------------------------------------------------------
1 | extends Reference
2 |
3 | # See details: https://wiki.sc4devotion.com/index.php?title=DBDF
4 |
5 | class_name DBDF #Database Directory Files
6 |
7 | var entries = []
8 |
9 | func load(file, location, size):
10 | file.seek(location)
11 | for _i in range(size / 16):
12 | entries.append(DBDFEntry.new(file))
13 |
--------------------------------------------------------------------------------
/addons/dbpf/DBDFEntry.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 | class_name DBDFEntry
3 |
4 | var type_id
5 | var group_id
6 | var instance_id
7 | var final_size
8 |
9 | func _init(file):
10 | self.type_id = file.get_32()
11 | self.group_id = file.get_32()
12 | self.instance_id = file.get_32()
13 | self.final_size = file.get_32()
14 |
--------------------------------------------------------------------------------
/addons/dbpf/DBPF.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends Resource
18 |
19 | # See details: https://wiki.sc4devotion.com/index.php?title=DBPF
20 |
21 | class_name DBPF # Database Packed File
22 |
23 | var subfiles : Dictionary
24 | var compressed_files : Dictionary
25 | var indices : Dictionary
26 | var indices_by_type : Dictionary
27 | var indices_by_type_and_group : Dictionary
28 | var all_types : Dictionary
29 | var file : File
30 | var path : String
31 | var print_load_times : bool = false
32 |
33 | export (Dictionary) var ui_region_textures: Dictionary = {}
34 |
35 | func _init(filepath : String):
36 | self.path = filepath
37 | var total_time_start = OS.get_system_time_msecs()
38 | # Open the file
39 | self.file = File.new()
40 | var err = file.open(filepath, File.READ)
41 | if err != OK:
42 | return err
43 | # Read the file
44 | # Check that the first four bytes are DBPF
45 | var dbpf = self.file.get_buffer(4).get_string_from_ascii();
46 | if (dbpf != "DBPF"):
47 | return ERR_INVALID_DATA
48 | # Get the version
49 | var _version_major = self.file.get_32()
50 | var _version_minor = self.file.get_32()
51 | # useless bytes
52 | for _i in range(3):
53 | file.get_32()
54 | var _date_created = OS.get_datetime_from_unix_time(self.file.get_32())
55 | var _date_modified = OS.get_datetime_from_unix_time(self.file.get_32())
56 | var _index_major_version = self.file.get_32()
57 | var index_entry_count = self.file.get_32()
58 | var index_first_offset = self.file.get_32()
59 | var _hole_entry_count = self.file.get_32()
60 | var _hole_offset = self.file.get_32()
61 | var _hole_size = self.file.get_32()
62 | var _index_minor_version = self.file.get_32()
63 | var _index_offset = self.file.get_32()
64 | var _unknown = self.file.get_32()
65 |
66 | self.file.seek(index_first_offset)
67 | var index_buffer = StreamPeerBuffer.new()
68 | index_buffer.data_array = self.file.get_buffer(index_entry_count * 20)
69 | var time_start = OS.get_system_time_msecs()
70 | for _i in range(index_entry_count):
71 | var index = SubfileIndex.new(self, index_buffer)
72 | self.indices[SubfileTGI.TGI2str(index.type_id, index.group_id, index.instance_id)] = index
73 |
74 | if not index.type_id in indices_by_type:
75 | indices_by_type[index.type_id] = [index]
76 | else:
77 | indices_by_type[index.type_id].append(index)
78 |
79 | # This is for debugging purposes. Currently unused, disabled to save loading time
80 | #if not [index.type_id, index.group_id] in indices_by_type_and_group:
81 | # indices_by_type_and_group[SubfileTGI.TG2int(index.type_id, index.group_id)] = [index]
82 | #else:
83 | # indices_by_type_and_group[SubfileTGI.TG2int(index.type_id, index.group_id)].append(index)
84 | var time_now = OS.get_system_time_msecs()
85 | if self.print_load_times:
86 | print("Took ", time_now - time_start, "ms to read ", index_entry_count, " indices from ", filepath)
87 |
88 | # Find compressed file and mark them
89 | for index in indices_by_type.get(0xe86b1eef, []):
90 | var dbdf = DBDF.new()
91 | dbdf.load(file, index.location, index.size)
92 | for compressed_file in dbdf.entries:
93 | compressed_files[SubfileTGI.TGI2str(compressed_file.type_id, compressed_file.group_id, compressed_file.instance_id)] = compressed_file
94 | var total_time_now = OS.get_system_time_msecs()
95 | if self.print_load_times:
96 | print("Took ", total_time_now - total_time_start, "ms to load ", filepath)
97 |
98 | func dbg_subfile_types():
99 | for index in indices.values():
100 | var subfile_type = SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id).split("\t")[0]
101 | if subfile_type == null:
102 | subfile_type = "%08x" % index.type_id
103 | if all_types.has(subfile_type):
104 | all_types[subfile_type] += 1
105 | else:
106 | all_types[subfile_type] = 1
107 | print("All types found:")
108 | for type in all_types:
109 | print("%s: %d" % [type, all_types[type]])
110 |
111 | func dbg_show_all_subfiles():
112 | print("=== %s" % self.path)
113 | print("=== ALL SUBFILES ===")
114 | for index in indices.values():
115 | print("%s (%d B)" % [SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id), index.size])
116 | print("====================")
117 |
118 | func DEBUG_show_all_subfiles_to_file(filename):
119 | print("=== %s" % self.path)
120 | print("=== ALL SUBFILES ===")
121 | var file = File.new()
122 | var name = "user://%s.txt" % [filename.split('/')[1]]
123 | print(name)
124 | var err = file.open(name, file.WRITE)
125 | print("error", err)
126 | file.seek(0)
127 | for index in indices.values():
128 | var string = "%s (%d B)" % [SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id), index.size]
129 | file.store_line(string)
130 | #print(string)
131 | file.close()
132 | print("====================")
133 |
134 | func all_subfiles_by_group(group_id : int):
135 | print("=== ALL SUBFILES BY GROUP %08x ===" % group_id)
136 | for index in indices.values():
137 | if index.group_id == group_id:
138 | print("%s (%d B)" % [SubfileTGI.get_file_type(index.type_id, index.group_id, index.instance_id), index.size])
139 | print("====================")
140 |
141 | func get_subfile(type_id : int, group_id : int, instance_id : int, subfile_class) -> DBPFSubfile:
142 | assert(SubfileTGI.TGI2str(type_id, group_id, instance_id) in self.indices,
143 | "Subfile not found (%08x %08x %08x)" % [type_id, group_id, instance_id])
144 |
145 | if subfiles.has([type_id, group_id, instance_id]) and subfiles[[type_id, group_id, instance_id]] != null:
146 | return subfiles[[type_id, group_id, instance_id]]
147 |
148 | var index : SubfileIndex = self.indices[SubfileTGI.TGI2str(type_id, group_id, instance_id)]
149 |
150 | # If the file is in the DBDF, then it's compressed
151 | var dbdf : DBDFEntry = self.compressed_files.get(SubfileTGI.TGI2str(index.type_id, index.group_id, index.instance_id), null)
152 |
153 | var subfile : DBPFSubfile = subfile_class.new(index)
154 | subfiles[[index.type_id, index.group_id, index.instance_id]] = subfile
155 | subfile.load(self.file, dbdf)
156 | return subfile
157 |
--------------------------------------------------------------------------------
/addons/dbpf/DBPFPlugin.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends EditorPlugin
3 | class_name DBPFPlugin
4 |
5 | func _enter_tree():
6 | self.add_custom_type('DBPF', 'Resource', load("res://addons/dbpf/DBPF.gd"), load("res://addons/dbpf/dbpf.png"))
7 |
8 | func _exit_tree():
9 | pass
10 |
--------------------------------------------------------------------------------
/addons/dbpf/DBPFSubfile.gd:
--------------------------------------------------------------------------------
1 | extends Reference
2 | class_name DBPFSubfile
3 |
4 | var index:SubfileIndex
5 | var raw_data:PoolByteArray
6 | var stream:StreamPeerBuffer
7 |
8 | func _init(idx:SubfileIndex):
9 | self.index = idx
10 |
11 | func load(file:File, dbdf:DBDFEntry=null):
12 | file.seek(index.location)
13 | if dbdf != null:
14 | raw_data = decompress(file, index.size - 9, dbdf)
15 | else:
16 | raw_data = file.get_buffer(index.size)
17 | stream = StreamPeerBuffer.new()
18 | stream.data_array = raw_data
19 |
20 | func decompress(file : File, length : int, dbdf : DBDFEntry) -> PoolByteArray:
21 | var buf:PoolByteArray
22 | var answer:PoolByteArray = PoolByteArray()
23 | var numplain:int
24 | var numcopy:int
25 | var offset:int
26 | var byte1:int
27 | var byte2:int
28 | var byte3:int
29 | var fromoffset:int
30 |
31 | file.get_32() # 4 redundant bytes
32 | file.get_16() # Compression type
33 | var decompressed_size = file.get_8() * 256 * 256
34 | decompressed_size += file.get_8() * 256
35 | decompressed_size += file.get_8()
36 | if decompressed_size != dbdf.final_size:
37 | print("WARNING: decompressed size does not match expected size")
38 | print("Expected: %d" % dbdf.final_size)
39 |
40 | while (length > 0):
41 | var cc = file.get_8()
42 | length -= 1
43 | byte1 = 0
44 | byte2 = 0
45 | byte3 = 0
46 | if cc >= 252:
47 | numplain = cc & 0x03
48 | if numplain > length:
49 | numplain = length
50 | numcopy = 0
51 | offset = 0
52 | elif cc >= 224:
53 | numplain = (cc - 0xdf) << 2
54 | numcopy = 0
55 | offset = 0
56 | elif cc >= 192:
57 | length -= 3
58 | byte1 = file.get_8()
59 | byte2 = file.get_8()
60 | byte3 = file.get_8()
61 | numplain = cc & 0x03
62 | numcopy = ((cc & 0x0c) << 6) + 5 + byte3
63 | offset = ((cc & 0x10) << 12) + (byte1 << 8) + byte2
64 | elif cc >= 128:
65 | length -= 2
66 | byte1 = file.get_8()
67 | byte2 = file.get_8()
68 | numplain = (byte1 & 0xc0) >> 6
69 | numcopy = (cc & 0x3f) + 4
70 | offset = ((byte1 & 0x3f) << 8) + byte2
71 | else:
72 | length -= 1
73 | byte1 = file.get_8()
74 | numplain = (cc & 0x03)
75 | numcopy = ((cc & 0x1c) >> 2) + 3
76 | offset = ((cc & 0x60) << 3) + byte1
77 |
78 | length -= numplain
79 | if (numplain > 0):
80 | buf = file.get_buffer(numplain)
81 | answer.append_array(buf)
82 |
83 | fromoffset = len(answer) - (offset + 1)
84 | for i in range(numcopy):
85 | answer.append(answer[fromoffset+i])
86 |
87 | return answer
88 |
--------------------------------------------------------------------------------
/addons/dbpf/ExemplarSubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 |
3 | class_name ExemplarSubfile
4 |
5 | var parent_cohort = {}
6 | var num_properties: int
7 | var properties = {}
8 | var ind
9 | var keys_dict = {}
10 |
11 | func _init(index).(index):
12 | pass
13 |
14 | func load(file, dbdf=null):
15 | .load(file, dbdf)
16 | file.seek(index.location)
17 | ind = 0
18 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
19 | # 4 bytes (char) - signature
20 | var signature = raw_data.subarray(ind, ind+3).get_string_from_ascii()
21 | assert(signature == "EQZB", "DBPFSubfile.load: not an Exemplar file")
22 | ind += 4
23 | # 4 bytes - parent cohort indicator always 0x23232331
24 | ind += 4
25 | self.parent_cohort["T"] = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
26 | ind += 4
27 | self.parent_cohort["G"] = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
28 | ind += 4
29 | self.parent_cohort["I"] = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
30 | ind += 4
31 | self.num_properties = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
32 | ind += 4
33 | for _property in range(self.num_properties):
34 | var key = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
35 | ind += 4
36 | # 1 Byte spacing always 0x00
37 | ind += 1
38 | var type = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
39 | ind += 4
40 | var multi :bool = (type & 0xF000) > 0
41 | var format :int = type & 0xF
42 | var value
43 | var length = 1
44 | if multi:
45 | length = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
46 | ind += 4
47 | if format == 0xC: #string
48 | value = raw_data.subarray(ind, ind+(length-1)).get_string_from_ascii()
49 | ind += length
50 | else:
51 | value = []
52 | for _i in range(length):
53 | value.append(self.val_from_format(format))
54 | else:
55 | value = self.val_from_format(format)
56 | self.properties[key] = value
57 | return OK
58 |
59 |
60 | func get_int_from_bytes(bytearr):
61 | var r_int = 0
62 | var shift = 0
63 | for byte in bytearr:
64 | r_int = (r_int) | (byte << shift)
65 | shift += 8
66 | return r_int
67 |
68 | func get_float_from_bytes(bytearr):
69 | var buff = StreamPeerBuffer.new()
70 | buff.data_array = bytearr
71 | return buff.get_float()
72 |
73 | func val_from_format(format):
74 | "storing less thant 32bit ints as 32bit ints seems bad, should do this in C++ to remedy that"
75 | if format == 0x1: #Uint8
76 | var val = raw_data[ind]
77 | ind += 1
78 | return val
79 | elif format == 0x2: #Uint16 not used?
80 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
81 | ind += 2
82 | return val
83 | elif format == 0x3: #Uint32
84 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
85 | ind += 4
86 | return val
87 | elif format == 0x4: #Uint64? not used?
88 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+7))
89 | ind += 8
90 | return val
91 | elif format == 0x5: #int8? not used?
92 | var val = raw_data[ind]
93 | ind += 1
94 | return val
95 | elif format == 0x6: #int16? not used?
96 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
97 | ind += 2
98 | return val
99 | elif format == 0x7: #int32? not used?
100 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
101 | ind += 4
102 | return val
103 | elif format == 0x8: #int64? not used?
104 | var val = self.get_int_from_bytes(raw_data.subarray(ind, ind+7))
105 | ind += 8
106 | return val
107 | elif format == 0x9: #float32
108 | var val = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))
109 | ind += 4
110 | return val
111 | elif format == 0xA: #float64?
112 | var val = self.get_float_from_bytes(raw_data.subarray(ind, ind+7))
113 | ind += 8
114 | return val
115 | elif format == 0xB: #bool
116 | var val = raw_data[ind]
117 | ind += 1
118 | return (val != 0)
119 | else:
120 | print("ERROR, unkown format: %d", format)
121 |
122 | func key_description(key):
123 | if len(self.keys_dict) == 0:
124 | var file = File.new()
125 | file.open("res://exemplar_types.dict", File.READ)
126 | self.keys_dict = str2var(file.get_as_text())
127 | file.close()
128 | var ret = null
129 | if self.keys_dict.keys().has(key):
130 | ret = self.keys_dict[key]
131 | return ret
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/addons/dbpf/FSHSubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 |
3 | class_name FSHSubfile
4 |
5 | var img
6 | var width
7 | var height
8 | var size
9 | var mipmaps
10 | var file_size
11 |
12 | func _init(index).(index):
13 | pass
14 |
15 | func load(file, dbdf=null):
16 | .load(file, dbdf)
17 | file.seek(index.location)
18 | var ind = 0
19 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
20 | # 4 bytes (char) - signature
21 | var signature = raw_data.subarray(ind, ind+3).get_string_from_ascii()
22 | assert(signature == "SHPI", "DBPFSubfile.load: not an FSH file")
23 | ind += 4
24 | # 4 bytes (unint32) - total file size
25 | self.file_size = self._get_int_from_bytes(raw_data.subarray(ind, ind+3))
26 | ind += 4
27 | # 4 bytes (uint32) - number of entries
28 | var num_entr : int = self._get_int_from_bytes(raw_data.subarray(ind, ind+3))
29 | ind += 4
30 | # 4 bytes (char) - directory ID
31 | var dir_id = raw_data.subarray(ind, ind+3).get_string_from_ascii()
32 | ind += 4
33 |
34 | # Directory
35 | var dir_bytes = raw_data.subarray(ind, ind+7)
36 | var directory = []
37 |
38 | while len(directory) < num_entr:
39 | # 4 bytes (char) - entry tag // e.g. "br02"
40 | var tag = dir_bytes.subarray(0, 3).get_string_from_ascii()
41 | ind += 4
42 | # 4 bytes (uint32) - entry offset
43 | var offset : int = self._get_int_from_bytes(dir_bytes.subarray(4, 7))
44 | ind += 4
45 | directory.append(FSH_Entry.new(tag, offset))
46 | dir_bytes = raw_data.subarray(ind, ind+7)
47 | assert (ind < file_size, "error")
48 |
49 | # optional binary attachment (padding)
50 | # Note: this attachment is added only for 16 bytes alignment
51 | # 8 bytes (char) - ID string // "Buy ERTS":
52 | var id_str = dir_bytes.get_string_from_ascii()
53 | ind += 8
54 | var pad = (int(ind/16))*16
55 | if pad != ind:
56 | ind = pad+16
57 | var entries = {}
58 | var dir_ind = 0
59 | # 1 byte (uint8) - entry ID // 0x69
60 | for entry in directory:
61 |
62 | if dir_ind != 0:
63 | var prev = directory[dir_ind-1]
64 | prev.size = entry.offset - prev.offset
65 | dir_ind += 1
66 | ind = entry.offset
67 | entry.size = self.file_size - entry.offset
68 | # data
69 | # -header 16 bytes;
70 | # 1 byte (uint8) - record ID / entry type / image type
71 | entry.entry_id = raw_data[ind]
72 | ind += 1
73 | # 3 bytes (uint24) - size of the block not used
74 | entry.block_size = self._get_int_from_bytes(raw_data.subarray(ind, ind+2))
75 | ind += 3
76 | # 2 bytes (uint16) - image width
77 | entry.width = self._get_int_from_bytes(raw_data.subarray(ind, ind+1))
78 | ind += 2
79 | # 2 bytes (uint16) - image height
80 | entry.height = self._get_int_from_bytes(raw_data.subarray(ind, ind+1))
81 | ind += 2
82 | # 2 bytes (uint16) - X axis coordinate (Center X)
83 | entry.x_coord = self._get_int_from_bytes(raw_data.subarray(ind, ind+1))
84 | ind += 2
85 | # 2 bytes (uint16) - Y axis coordinate (Center Y)
86 | entry.y_coord = self._get_int_from_bytes(raw_data.subarray(ind, ind+1))
87 | ind += 2
88 | # 2 bytes - X axis position (Left X pos.)[uint12] + internal flag [uint1] + unknown [uint3]
89 | entry.x_pos = self._get_int_from_bytes(raw_data.subarray(ind, ind+1)) >> 2
90 | ind += 2
91 | # 2 bytes - Y axis position (Top Y pos.)[uint12] + levels count (mipmaps) [uint4]
92 | entry.y_pos = self._get_int_from_bytes(raw_data.subarray(ind, ind+1)) >> 2
93 | entry.mipmaps = 3&self._get_int_from_bytes(raw_data.subarray(ind, ind+1))
94 | ind += 2
95 | # x bytes - image data
96 | # x bytes - optional padding // up to 16 bytes, filled with nulls
97 | # x bytes - optional palette (header + data)
98 | # x bytes - optional binary attachments (header + data)
99 |
100 |
101 | for entry in directory:
102 |
103 | var start = entry.offset+16
104 | var att_id = entry.entry_id
105 |
106 | entry.img = Image.new()
107 | if att_id == 96: # compressed image, DXT1 4x4 packed, 1-bit alpha
108 | entry.size = ((entry.width * entry.height) / 16)*8
109 | var img_data = raw_data.subarray(start, start + entry.size-1)
110 | entry.img.create_from_data(entry.width, entry.height, false, Image.FORMAT_DXT1, img_data)
111 | elif att_id == 97: # compressed image, DXT3 4x4 packed, 4-bit alpha
112 | entry.size = ((entry.width * entry.height) / 16)*16
113 | var img_data = raw_data.subarray(start, start + entry.size-1)
114 | entry.img.create_from_data(entry.width, entry.height, false, Image.FORMAT_DXT3, img_data)
115 | """elif att_id == 123 or att_id == 125 or att_id == 127: # image with palette (256 colors), 24 and 32 bmp
116 | entry.img.load_bmp_from_buffer(img_data)"""
117 | assert(entry.img != null, "img load failed")
118 | self.img = entry.img
119 | self.width = entry.width
120 | self.height = entry.height
121 | return OK
122 |
123 |
124 | func _get_int_from_bytes(bytearr):
125 | var r_int = 0
126 | var shift = 0
127 | for byte in bytearr:
128 | r_int = (r_int) | (byte << shift)
129 | shift += 8
130 | return r_int
131 |
132 | func get_as_texture():
133 | assert(self.img != null)
134 | var ret = ImageTexture.new()
135 | ret.create_from_image(self.img, 2)
136 | return ret
137 |
--------------------------------------------------------------------------------
/addons/dbpf/FSH_Entry.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | class_name FSH_Entry
4 |
5 | var dir_tag
6 | var entry_id
7 | var offset
8 | var size
9 | var block_size
10 | var width
11 | var height
12 | var x_coord
13 | var y_coord
14 | var x_pos
15 | var y_pos
16 | var mipmaps
17 | var img
18 |
19 | func _init(tag, offset):
20 | self.dir_tag = tag
21 | self.offset = offset
22 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWin.gd:
--------------------------------------------------------------------------------
1 | extends Control
2 | class_name GZWin
3 |
4 | # Base class for the GUI reimplementation of SC4
5 |
6 | func _init(attributes : Dictionary):
7 | var area = attributes.get('area', Rect2())
8 | self.visible = attributes.get('winflag_visible', true)
9 | self.set_position(area.position)
10 | self.set_size(area.size)
11 | if 'tipstext' in attributes:
12 | self.hint_tooltip = attributes.get('hint_tooltip')
13 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWinBMP.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends GZWin
18 | class_name GZWinBMP
19 |
20 | var texture : Texture = null
21 |
22 | func _init(attributes).(attributes):
23 | if not 'image' in attributes:
24 | print(attributes)
25 | else:
26 | if attributes['image'] == null:
27 | set_texture(load("res://missing_subfile.png"))
28 | else:
29 | set_texture(attributes['image'].get_as_texture())
30 |
31 | func set_texture(texture : Texture):
32 | self.texture = texture
33 | update()
34 |
35 | func _draw():
36 | if self.texture != null:
37 | draw_texture(texture, get_position())
38 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWinBtn.gd:
--------------------------------------------------------------------------------
1 | extends GZWin
2 | class_name GZWinBtn
3 |
4 | # Texture order:
5 | # 0. disabled
6 | # 1. normal
7 | # 2. pressed
8 | # 3. hover
9 |
10 | enum ButtonState {
11 | DISABLED = 0,
12 | NORMAL = 1,
13 | PRESSED = 2,
14 | HOVER = 3,
15 | CHECKBOX_DISABLED = 6,
16 | CHECKBOX_PRESSED = 4,
17 | CHECKBOX_HOVER = 2,
18 | }
19 |
20 | enum ButtonStyle {
21 | STANDARD = 0,
22 | RADIOCHECK,
23 | TOGGLE
24 | }
25 |
26 | signal clicked
27 | signal toggled_on
28 | signal toggled_off
29 | signal checked
30 | signal unchecked
31 |
32 | var state = 0
33 | # Checkboxes and radio buttons seem to have eight states
34 | var textures : Array = [null, null, null, null, null, null, null, null]
35 | var N_SUBTEXTURES : int = 4
36 | var style =ButtonStyle.STANDARD
37 | var is_toggled : bool = false
38 | var is_hovered : bool = false
39 | var is_pressed : bool = false
40 | var is_checked : bool = false
41 | var is_disabled : bool = false
42 |
43 | func _init(attributes : Dictionary).(attributes):
44 | var style = attributes.get('style', 'standard')
45 | match style:
46 | "standard":
47 | self.style = ButtonStyle.STANDARD
48 | N_SUBTEXTURES = 4
49 | "radiocheck":
50 | self.style = ButtonStyle.RADIOCHECK
51 | N_SUBTEXTURES = 8
52 | "toggle":
53 | self.style = ButtonStyle.TOGGLE
54 | N_SUBTEXTURES = 4
55 | if 'image' in attributes:
56 | set_texture(attributes['image'].get_as_texture())
57 | if 'imagerect' in attributes:
58 | print("Imagerect", attributes['imagerect'])
59 | update_state()
60 |
61 | func set_texture(texture : Texture):
62 | set_size(Vector2(texture.get_width() / N_SUBTEXTURES, texture.get_height()))
63 | for i in N_SUBTEXTURES:
64 | textures[i] = get_cropped_texture(texture, Rect2(i * texture.get_width() / N_SUBTEXTURES, 0, texture.get_width() / N_SUBTEXTURES, texture.get_height()))
65 |
66 | func get_cropped_texture(texture : Texture, region : Rect2):
67 | var atlas_texture = AtlasTexture.new()
68 | atlas_texture.set_atlas(texture)
69 | atlas_texture.set_region(region)
70 | return atlas_texture
71 |
72 | func get_minimum_size():
73 | return get_size()
74 |
75 | func _draw():
76 | draw_texture(self.textures[self.state], Vector2())
77 |
78 | func update_state():
79 | if self.style == ButtonStyle.STANDARD or self.style == ButtonStyle.TOGGLE:
80 | if is_disabled:
81 | self.state = ButtonState.DISABLED
82 | elif is_pressed or is_toggled:
83 | self.state = ButtonState.PRESSED
84 | elif is_hovered:
85 | self.state = ButtonState.HOVER
86 | else:
87 | self.state = ButtonState.NORMAL
88 | else:
89 | self.state = 0
90 | if not is_checked:
91 | self.state += 1
92 | if is_disabled:
93 | self.state += ButtonState.CHECKBOX_DISABLED
94 | elif is_pressed:
95 | self.state += ButtonState.CHECKBOX_PRESSED
96 | elif is_hovered:
97 | self.state += ButtonState.CHECKBOX_HOVER
98 | update()
99 |
100 | func _gui_input(event):
101 | if is_disabled:
102 | return
103 | if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
104 | if event.pressed:
105 | is_pressed = true
106 | if self.style == ButtonStyle.RADIOCHECK:
107 | is_checked = not is_checked
108 | if is_checked:
109 | emit_signal('checked')
110 | else:
111 | emit_signal('unchecked')
112 | elif self.style == ButtonStyle.TOGGLE:
113 | is_toggled = not is_toggled
114 | if is_toggled:
115 | emit_signal('toggled_on')
116 | else:
117 | emit_signal('toggled_off')
118 | else:
119 | emit_signal('clicked')
120 | else:
121 | is_pressed = false
122 | update_state()
123 |
124 | func _notification(what):
125 | match what:
126 | NOTIFICATION_MOUSE_ENTER:
127 | if not is_disabled:
128 | is_hovered = true
129 | update_state()
130 |
131 | NOTIFICATION_MOUSE_EXIT:
132 | if not is_disabled:
133 | is_hovered = false
134 | update_state()
135 |
136 | func set_state(state):
137 | self.state = state
138 | update()
139 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWinFlatRect.gd:
--------------------------------------------------------------------------------
1 | extends GZWin
2 | class_name GZWinFlatRect
3 |
4 | var color : Color = Color(1, 1, 1)
5 |
6 | var nofill = false
7 |
8 | func _init(attributes : Dictionary).(attributes):
9 | print(attributes)
10 | if attributes.get("style", "") == "nofill":
11 | nofill = true
12 | update()
13 |
14 | func _draw():
15 | pass
16 | #TODO: should be capable of drawing each border with a different color
17 | if false:
18 | draw_rect(Rect2(Vector2(), self.get_size()), color, not nofill)
19 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWinGen.gd:
--------------------------------------------------------------------------------
1 | extends GZWin
2 | class_name GZWinGen
3 |
4 | func _init(attributes : Dictionary).(attributes):
5 | update()
6 |
7 | func _draw():
8 | pass
9 |
--------------------------------------------------------------------------------
/addons/dbpf/GZWinText.gd:
--------------------------------------------------------------------------------
1 | extends GZWin
2 | class_name GZWinText
3 |
4 | var text : String
5 | var font = null
6 |
7 | func _init(attributes).(attributes):
8 | # hack to get the default font while we can't decode the Simcity 4 ones
9 | var label = Label.new()
10 | self.font = label.get_font("")
11 | label.free()
12 | self.set_text(attributes.get('caption', ''))
13 | if attributes.has('captionres'):
14 | # Read hex reference
15 | # The format is as follows: 0x{group_id,instance_id}
16 | Logger.info("Captionres value: %s" % attributes['captionres'])
17 | var captionres = attributes['captionres'].trim_prefix('{').trim_suffix('}').split(',')
18 | var group_id = ("0x%s"%captionres[0]).hex_to_int()
19 | var instance_id = ("0x%s"%captionres[1]).hex_to_int()
20 | # Read string from subfile
21 | var ltext_subfile = Core.subfile(0x2026960B, group_id, instance_id, LTEXTSubfile)
22 | if ltext_subfile != null:
23 | self.set_text(ltext_subfile.text)
24 | else:
25 | self.set_text("%s || ERROR"%attributes.get('caption', 'no caption defined'))
26 |
27 | func set_text(text : String):
28 | self.text = text
29 | self.update()
30 |
31 | func _draw():
32 | draw_string(font, Vector2(0, font.get_height()), self.text, Color.white)
33 |
34 |
--------------------------------------------------------------------------------
/addons/dbpf/INISubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 | class_name INISubfile
3 |
4 | var sections = {}
5 | var file_path
6 | var cfgFile : ConfigFile
7 |
8 | func _init(index).(index):
9 | pass
10 |
11 | func load(file, dbdf=null):
12 | .load(file, dbdf)
13 | file.seek(index.location)
14 |
15 | # var current_section = ""
16 | # if err != OK:
17 | # Logger.error("Couldn't load file %s. Error: %s " % file_path, err )
18 | # return err
19 | # while ! file.eof_reached():
20 | # var line = file.get_line()
21 | # line = line.strip_edges(true, true)
22 | # if line.length() == 0:
23 | # continue
24 | # if line[0] == '#' or line[0] == ';':
25 | # continue
26 | # if line[0] == '[':
27 | # current_section = line.substr(1, line.length() - 2)
28 | # sections[current_section] = {}
29 | # else:
30 | # var key = line.split('=')[0]
31 | # var value = line.split('=')[1]
32 | # sections[current_section][key] = value
33 |
34 | #DebugUtils.print_dict(sections, self)
35 |
36 |
37 | func get_as_cfg():
38 | var sections = {}
39 | var err = cfgFile.load(raw_data.get_string_from_ascii())
40 | if err != OK:
41 | Logger.error("Couldnt load as INI!")
42 | return {}
43 | else:
44 | pass
45 |
46 | func save_file(name):
47 | cfgFile.save(name)
48 |
--------------------------------------------------------------------------------
/addons/dbpf/ImageSubfile.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends DBPFSubfile
18 |
19 | class_name ImageSubfile
20 |
21 | var img
22 |
23 | func _init(index).(index):
24 | pass
25 |
26 | func load(file, dbdf=null):
27 | .load(file, dbdf)
28 | file.seek(index.location)
29 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
30 | assert(raw_data[0] == 0x89 and raw_data[1] == 0x50 and raw_data[2] == 0x4E and raw_data[3] == 0x47, "DBPFSubfile.load: invalid magic")
31 | self.img = Image.new()
32 | var err = img.load_png_from_buffer(raw_data)
33 | if err != OK:
34 | return err
35 | return OK
36 |
37 | func get_as_texture():
38 | assert(self.img != null)
39 | var ret = ImageTexture.new()
40 | ret.create_from_image(self.img, 0)
41 | return ret
42 |
--------------------------------------------------------------------------------
/addons/dbpf/LTEXTSubfile.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends DBPFSubfile
18 |
19 | class_name LTEXTSubfile
20 |
21 | var text : String
22 |
23 | func _init(index).(index):
24 | pass
25 |
26 | func load(file, dbdf=null):
27 | .load(file, dbdf)
28 | text = ""
29 | var n_characters = stream.get_u16() # (2-byte unicode characters)
30 | # Check that we have the correct amount of characters
31 | var expected_characters = (stream.get_available_bytes() - 2) / 2
32 | if expected_characters < n_characters:
33 | print("LTEXTSubfile: Too few characters (%d < %d)" % [expected_characters, n_characters])
34 | var control = stream.get_u16()
35 | if control != 0x0010:
36 | Logger.debug("Wrong control code: 0x%04x (expects 0x0010)" % control)
37 | pass
38 |
39 | for i in range(n_characters):
40 | if stream.get_available_bytes() < 2:
41 | print("LTEXTSubfile: Unexpected end of stream")
42 | break
43 | text += char(stream.get_u16())
44 |
--------------------------------------------------------------------------------
/addons/dbpf/RULSubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 |
3 | class_name RULSubfile
4 |
5 | """
6 | Individual Network RULs:
7 | use edge-shape definitions to define what object to render there,
8 | it doesn't specify wether it is an FSH or an S3D or an Exemplar
9 |
10 | my approach will be to store the RUL information in nested dicts
11 | dict[W = dict[N = dict[E = dict[S = IID]]]]
12 | This data is then used to add the FSH's to texturearray(s)
13 | for S3D objects I'd need to initialize them and get their FSH's
14 | when adding FSH's to the array I ofcourse need to keep track of what layer is what FSH
15 | that record should also allow me to not load duplicate FSH's since they are reused
16 | This data is then used to define a dict of the same shape with TransitTile objects
17 | that store path and texarrlayer data
18 |
19 | Edge definitions come in the following forms:
20 | per side w, n, e, s:
21 | - 00 = disconnected
22 | - 01 = diagonal 45deg left (as viewed from edge to center)
23 | - 02 = straight
24 | - 03 = diagonal 45deg right
25 | - 04 = shared median for 2-tile networks
26 | - 11 = transition from 01 to 02, diag to straight
27 | - 13 = transition as 11 but other diagonal
28 | rails also use:
29 | - 21: N-0x03187F00,0,0
30 | - 22: S-0x03002200,0,0
31 | - 23: E-0x03001C00,0,0
32 |
33 | - 32: S-0x03031700,0,0
34 | - 42: S-0x03034b00,0,0
35 | - 52: N-0x0306BD00,0,0
36 | - 62: E-0x03014E00,0,0
37 | - 72: N-0x0312FF00,0,0
38 | 21 to 52 describe sides with in-between diagonals where 62 and 72 describe sides with double diagonals from two directions with 72 including a straight aswell
39 | - ?: specifies edges that are irrelevant to tiles being defined.
40 |
41 | My observation:
42 | Network Instances starting hex wnes_keys
43 | example id of straight piece
44 | 0d - monorail 0x0d031500 - FSH & S3D -dat2 & dat1
45 | 0c - bridges
46 | 0b - bridges
47 | 0a - ground highway 0x0a001500 - S3D [FSH is reused and stretched between parts]
48 | 09 - one way road 0x09004B00 - FSH -dat3
49 | 08 - el-train or monorail 0x08031500 - FSH & S3D -dat3 & dat1 & mask in dat2
50 | 07 - subway 0x07004B00 - S3D -dat1 [needs translating]
51 | 06 - water-pipes 0x06004B00 - S3D -dat1 [needs translating]
52 | 05 - street 0x05004B00 - FSH -dat3 [needs translating for advanced rul or doesn't use it]
53 | 04 - avenue 0x04006100 - FSH -dat4
54 | 03 - rail 0x03031500 - FSH -dat4
55 | 02 - El Highway 0x02001500 - FSH & S3D -dat4 & dat1 [FSH isn't used, insead does what ground does]
56 | 00 - road 0x00004B00 - FSH -dat5
57 |
58 | from the wiki:
59 | 0x0000001 - Elevated Highway Basic RUL
60 | 0x0000002 - Elevated Highway Advanced RUL
61 | 0x0000003 - Pipe Basic RUL
62 | 0x0000004 - Pipe Advanced RUL
63 | 0x0000005 - Rail Basic RUL
64 | 0x0000006 - Rail Advanced RUL
65 | 0x0000007 - Road Basic RUL
66 | 0x0000008 - Road Advanced RUL
67 | 0x0000009 - Street Basic RUL
68 | 0x000000A - Street Advanced RUL
69 | 0x000000B - Subway Basic RUL
70 | 0x000000C - Subway Advanced RUL
71 | 0x000000D - Avenue Basic RUL
72 | 0x000000E - Avenue Advanced RUL
73 | 0x000000F - Elevated Rail Basic RUL
74 | 0x0000010 - Elevated Rail Advanced RUL
75 | 0x0000011 - One-Way Road Basic RUL
76 | 0x0000012 - One-Way Road Advanced RUL
77 | 0x0000013 - RHW ("Dirt Road") Basic RUL
78 | 0x0000014 - RHW ("Dirt Road") Advanced RUL
79 | 0x0000015 - Monorail Basic RUL
80 | 0x0000016 - Monorail Advanced RUL
81 | 0x0000017 - Ground Highway Basic RUL
82 | 0x0000018 - Ground Highway Advanced RUL
83 | """
84 |
85 | var RUL_wnes = {}
86 | var num_ids = 0
87 |
88 | func _init(index).(index):
89 | pass
90 |
91 | func load(file, dbdf=null):
92 | """
93 | stores RUL lines with 1-lines functioning as dict-keys
94 | 2 and 3 lines are stored as arrays in an 'entry-array'
95 | this is because there can be multiple entries with the same -tile edge-vals
96 | in order to handle that I will need to use the whole neighbor-grid as described below
97 | and evaluate the options and from the ones that fit use the one with the larges number of tiles that fit
98 | I'm hoping draw-cases would describe the same tile anyway
99 |
100 | neighbor location numbers (0 is base location described by 1-line):
101 | 11 12 13 14 15
102 | 10 2 3 4 16
103 | 9 1 0 5 17
104 | 24 8 7 6 18
105 | 23 22 21 20 19
106 | """
107 | .load(file, dbdf)
108 | file.seek(index.location)
109 | var ind = 0
110 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
111 | var ini_str = raw_data.get_string_from_ascii()
112 | var i = 0
113 | var raw_split = ini_str.split('\n')
114 | var ids_found = []
115 | while i < len(raw_split)-1:
116 | var line = raw_split[i].strip_edges(true, true)
117 | if len(line) > 1 and line[0] == '1':
118 | var split = line.split(",")
119 | var wnes_keys = []
120 | wnes_keys.append(int(split[1]))
121 | wnes_keys.append(int(split[2]))
122 | wnes_keys.append(int(split[3]))
123 | wnes_keys.append(int(split[4]))
124 | if not self.RUL_wnes.has(wnes_keys):
125 | self.RUL_wnes[wnes_keys] = []
126 | i+=1
127 | line = raw_split[i].strip_edges(true, true)
128 | var entry = []
129 | while line[0] != '1' and i < len(raw_split)-1:
130 | if line[0] == '2':
131 | var wnes2_keys = line.split(",")
132 | entry.append([
133 | int(wnes2_keys[0]),
134 | int(wnes2_keys[1]),
135 | int(wnes2_keys[2]),
136 | int(wnes2_keys[3]),
137 | int(wnes2_keys[4]),
138 | int(wnes2_keys[5])
139 | ])
140 | elif line[0] == '3':
141 | var wnes3_keys = line.split(",")
142 | var string = wnes3_keys[2].split("x")[1]
143 | # needed to split hex strings because godots hex_to_int is bugged for large numbers
144 | var hexstr_to_int = 0
145 | if len(string) > 4:
146 | hexstr_to_int = ("0x00" + string.substr(0, len(string)-4)).hex_to_int()<<16
147 | hexstr_to_int += ("0x00" + string.substr(max(len(string)-4, 0), len(string))).hex_to_int()
148 | entry.append([
149 | int(wnes3_keys[0]),
150 | int(wnes3_keys[1]),
151 | hexstr_to_int,
152 | int(wnes3_keys[3]),
153 | int(wnes3_keys[4])
154 | ])
155 | if not ids_found.has(hexstr_to_int):
156 | ids_found.append(hexstr_to_int)
157 | num_ids += 1
158 | i+=1
159 | while len(raw_split[i]) < 9 and i < len(raw_split)-1:
160 | i+=1
161 | if len(raw_split[i]) > 8:
162 | line = raw_split[i].strip_edges(true, true)
163 | self.RUL_wnes[wnes_keys].append(entry)
164 | else:
165 | i+=1
166 | return OK
167 |
--------------------------------------------------------------------------------
/addons/dbpf/S3DSubfile.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 |
3 | class_name S3DSubfile
4 |
5 | var groups = []
6 | var max_text_width = 0
7 | var max_text_height = 0
8 | var formats = []
9 |
10 | func _init(index).(index):
11 | pass
12 |
13 | func load(file, dbdf=null):
14 | .load(file, dbdf)
15 | file.seek(index.location)
16 | var ind = 0
17 | assert(len(raw_data) > 0, "DBPFSubfile.load: no data")
18 | # 4 bytes (char) - signature
19 | var signature = raw_data.subarray(ind, ind+3).get_string_from_ascii()
20 | assert(signature == "3DMD", "DBPFSubfile.load: not an FSH file")
21 | ind += 4
22 | # 4 bytes ? seems size and complexity related
23 | ind += 4
24 | "-HEAD block-"
25 | var h_head = raw_data.subarray(ind, ind+3).get_string_from_ascii()
26 | ind += 4
27 | var h_length = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
28 | ind += 4
29 | # seems to be always 1, might be anim related?
30 | ind += 2
31 | # seems to always be 5
32 | ind += 2
33 | if ind != h_length + 8:
34 | print("exception different HEAD length in %d", self.index.instance_id)
35 | ind = h_length + 8
36 |
37 | "-VERT block-"
38 | var v_vert = raw_data.subarray(ind, ind+3).get_string_from_ascii()
39 | ind += 4
40 | # seems field length for single group fields but gets freaky for multiple groups and animations
41 | ind += 4
42 | var v_grpcnt = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
43 | ind += 4
44 | var vertices = PoolVector3Array([])
45 | var UVs = PoolVector2Array([])
46 | for _grpind in range(v_grpcnt):
47 | var group = S3D_Group.new()
48 | self.groups.append(group)
49 | # 2 Bytes always 0?
50 | ind += 2
51 | var vert_count : int = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
52 | ind += 2
53 | var format : int = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
54 | ind += 4
55 | for _i in range(vert_count):
56 | var x = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))/16.0
57 | ind += 4
58 | var y = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))/16.0
59 | ind += 4
60 | var z = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))/16.0
61 | ind += 4
62 | vertices.append(Vector3(x, y, z))
63 | var u = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))
64 | ind += 4
65 | var v = self.get_float_from_bytes(raw_data.subarray(ind, ind+3))
66 | ind += 4
67 | UVs.append(Vector2(u, v))
68 |
69 | "-INDX block-"
70 | var i_indx = raw_data.subarray(ind, ind+3).get_string_from_ascii()
71 | ind += 4
72 | var i_length = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
73 | ind += 4
74 | var i_grpcnt = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
75 | ind += 4
76 | for grpind in range(i_grpcnt):
77 | # always 0?
78 | ind += 2
79 | # always 2?
80 | ind += 2
81 | var indxcnt = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
82 | ind += 2
83 | for _indxind in range(indxcnt):
84 | var vert_indx = self.get_int_from_bytes(raw_data.subarray(ind, ind+1))
85 | ind += 2
86 | var verts_tmp = self.groups[grpind].vertices
87 | verts_tmp.append(vertices[vert_indx])
88 | self.groups[grpind].vertices = verts_tmp
89 | var UVs_tmp = self.groups[grpind].UVs
90 | UVs_tmp.append(UVs[vert_indx])
91 | self.groups[grpind].UVs = UVs_tmp
92 |
93 | "-PRIM block-: this does nothing as everything seems to always just be triangles"
94 | var p_prim = raw_data.subarray(ind, ind+3).get_string_from_ascii()
95 | ind += 4
96 | var p_length = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
97 | ind += 4
98 | var p_grpcnt = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
99 | ind += 4
100 | for grpind in range(p_grpcnt):
101 | var p_type = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
102 | ind += 4
103 | if p_type != 1:
104 | print("unexpected primary type in %d", self.index.instance_id)
105 | # 6 Bytes ???
106 | ind += 6
107 | # 4 Bytes int number of vertices
108 | ind += 4
109 |
110 | "-MATS block-"
111 | var m_mats = raw_data.subarray(ind, ind+3).get_string_from_ascii()
112 | ind += 4
113 | var m_length = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
114 | ind += 4
115 | var m_grpcnt = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
116 | ind += 4
117 | for grpind in range(m_grpcnt):
118 | var settings = raw_data[ind]
119 | ind += 1
120 | if settings && 0x01:
121 | self.groups[grpind].alphatest = true
122 | if settings && 0x02:
123 | self.groups[grpind].depthtest = true
124 | if settings && 0x08:
125 | self.groups[grpind].backfacecull = true
126 | if settings && 0x10:
127 | self.groups[grpind].framebuffblnd = true
128 | if settings && 0x20:
129 | self.groups[grpind].texturing = true
130 | # 3 Bytes ?
131 | ind += 3
132 | self.groups[grpind].alphafunc = raw_data[ind]
133 | ind += 1
134 | self.groups[grpind].depthfunc = raw_data[ind]
135 | ind += 1
136 | self.groups[grpind].srcblend = raw_data[ind]
137 | ind += 1
138 | self.groups[grpind].destblend = raw_data[ind]
139 | ind += 1
140 | self.groups[grpind].alphathreshold = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
141 | ind += 4
142 | # 4 Bytes 0x01000000 some mask?
143 | ind += 4
144 | self.groups[grpind].mat_id = self.get_int_from_bytes(raw_data.subarray(ind, ind+3))
145 | ind += 4
146 | self.groups[grpind].wrapmodeU = raw_data[ind]
147 | ind += 1
148 | self.groups[grpind].wrapmodeV = raw_data[ind]
149 | ind += 1
150 | self.groups[grpind].magfilter = raw_data[ind]
151 | ind += 1
152 | self.groups[grpind].minfilter = raw_data[ind]
153 | ind += 1
154 | # 4 Bytes 0x00020021 (reset to zeroes when editing in reader)
155 | ind += 4
156 | var str_length = raw_data[ind]
157 | ind += 1
158 | self.groups[grpind].group_name = raw_data.subarray(ind, ind+str_length).get_string_from_ascii()
159 | ind += str_length
160 | # 1 Byte end string 0x00
161 | ind += 1
162 |
163 | "-ANIM block- TODO"
164 | "-PROP block- TODO"
165 | "-REGP block- TODO"
166 |
167 | func add_to_mesh(mesh: MeshInstance, location: Vector3):
168 | """this is temporary to test if it loads and how its size is compared to regulater terrain"""
169 | var vertices = PoolVector3Array([])
170 | var UVs = PoolVector2Array([])
171 | var images = []
172 | for group in self.groups:
173 | var loc_vert = PoolVector3Array([])
174 | var loc_UV = PoolVector2Array([])
175 | for vertind in range(group.vertices.size()-1, -1, -1):
176 | loc_vert.append(location + group.vertices[vertind])
177 | loc_UV.append(group.UVs[vertind])
178 | vertices.append_array(loc_vert)
179 | UVs.append_array(loc_UV)
180 | var image = get_texture_from_mat_id(group.mat_id)
181 | images.append(image)
182 | var textarr = TextureArray.new()
183 | textarr.create (self.max_text_width, self.max_text_height, len(self.groups), self.formats[0], 2)
184 | for imgind in range(len(images)):
185 | textarr.set_layer_data(images[imgind], imgind)
186 |
187 | var array_mesh : ArrayMesh = mesh.mesh
188 | var arrays : Array = []
189 | arrays.resize(ArrayMesh.ARRAY_MAX)
190 | arrays[ArrayMesh.ARRAY_VERTEX] = vertices
191 | arrays[ArrayMesh.ARRAY_TEX_UV] = UVs
192 | array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
193 | mesh.mesh = array_mesh
194 | var mat = mesh.get_material_override()
195 | mat.set_shader_param("s3dtexture", textarr)
196 | mesh.set_material_override(mat)
197 |
198 | func get_texture_from_mat_id(iid):
199 | var fsh_subfile = Core.subfile(
200 | 0x7ab50e44, 0x1ABE787D, iid, FSHSubfile
201 | )
202 | if self.max_text_width < fsh_subfile.width:
203 | self.max_text_width = fsh_subfile.width
204 | if self.max_text_height < fsh_subfile.height:
205 | self.max_text_height = fsh_subfile.height
206 | self.formats.append(fsh_subfile.img.get_format())
207 | return fsh_subfile.img
208 |
209 |
210 | func get_int_from_bytes(bytearr):
211 | var r_int = 0
212 | var shift = 0
213 | for byte in bytearr:
214 | r_int = (r_int) | (byte << shift)
215 | shift += 8
216 | return r_int
217 |
218 | func get_float_from_bytes(bytearr):
219 | var buff = StreamPeerBuffer.new()
220 | buff.data_array = bytearr
221 | return buff.get_float()
222 |
--------------------------------------------------------------------------------
/addons/dbpf/S3D_Group.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | class_name S3D_Group
4 |
5 | var vertices = PoolVector3Array([])
6 | var UVs = PoolVector2Array([])
7 | var mat_id: int
8 |
9 | # shader mat index, should handle picking the texturearray and layer that hold the mat
10 | var mat_index: int
11 |
12 | # s3d settings
13 | var alphatest: bool
14 | var depthtest: bool
15 | var backfacecull: bool
16 | var framebuffblnd: bool
17 | var texturing: bool
18 | var alphafunc: int
19 | var depthfunc: int
20 | var srcblend: int
21 | var destblend: int
22 | var alphathreshold: int
23 | var wrapmodeU: bool
24 | var wrapmodeV: bool
25 | var magfilter: int
26 | var minfilter: int
27 | var group_name: String
28 |
29 | func _init():
30 | pass
31 |
--------------------------------------------------------------------------------
/addons/dbpf/SubfileIndex.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | class_name SubfileIndex
4 |
5 | var type_id:int
6 | var group_id:int
7 | var instance_id:int
8 | var location:int
9 | var size:int
10 | var dbpf
11 |
12 | func _init(dbpf, buffer):
13 | type_id = buffer.get_u32()
14 | group_id = buffer.get_u32()
15 | instance_id = buffer.get_u32()
16 | location = buffer.get_u32()
17 | size = buffer.get_u32()
18 | self.dbpf = dbpf
19 |
--------------------------------------------------------------------------------
/addons/dbpf/SubfileTGI.gd:
--------------------------------------------------------------------------------
1 | # OpenSC4 - Open source reimplementation of Sim City 4
2 | # Copyright (C) 2023 The OpenSC4 contributors
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU Affero General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU Affero General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU Affero General Public License
15 | # along with this program. If not, see .
16 |
17 | extends Node
18 | class_name SubfileTGI
19 |
20 | # TODO: id to class mapping loaded only once?
21 |
22 | const TYPE_PNG = 0x856ddbac
23 | const GROUP_UI_IMAGE = 0x46a006b0
24 |
25 | static func TGI2str(type_id : int, group_id : int, instance_id : int) -> String:
26 | return "%08x%08x%08x" % [type_id, group_id, instance_id]
27 |
28 | static func TG2int(type_id : int, group_id : int) -> int:
29 | return type_id << 32 | group_id
30 |
31 | static func get_file_type(type_id : int, group_id : int, instance_id : int) -> String:
32 | var type = Core.type_dict_to_text.get(type_id, "0x%08x" % type_id)
33 | var group = Core.group_dict_to_text.get(group_id, "0x%08x" % group_id)
34 |
35 | return "%s %s 0x%08x" % [type, group, instance_id]
36 |
37 | # TODO: change this terrible name
38 | static func get_type_from_type(type_id : int) -> String:
39 | var type = "0x%08x" % type_id
40 | var type_dict = {
41 | 0x6534284a: "LTEXT",
42 | 0x5ad0e817: "S3D",
43 | 0x05342861: "Cohorts",
44 | 0x29a5d1ec: "ATC",
45 | 0x09ADCD75: "AVP",
46 | 0x7ab50e44: "FSH",
47 | 0xea5118b0: "EFFDIR",
48 | 0x856ddbac: "PNG",
49 | 0xca63e2a3: "LUA",
50 | 0xe86b1eef: "DBDF",
51 | 0x00000000: "TEXT"
52 | }
53 | if type_dict.has(type_id):
54 | type = type_dict[type_id]
55 | return type
56 |
57 | static func visualize_standalone(file : DBPFSubfile) -> void:
58 | var file_type = get_type_from_type(file.index.type_id)
59 | if file_type == "TEXT":
60 | print(file.data)
61 |
62 |
63 |
--------------------------------------------------------------------------------
/addons/dbpf/dbpf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenSC4-org/OpenSC4/fdc90c33e6e012b8c825f20ee10c51f55796df39/addons/dbpf/dbpf.png
--------------------------------------------------------------------------------
/addons/dbpf/dbpf.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/dbpf.png-7fcfe5f00851a7e63f4b76c9e4252da6.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/dbpf/dbpf.png"
13 | dest_files=[ "res://.import/dbpf.png-7fcfe5f00851a7e63f4b76c9e4252da6.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/addons/dbpf/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="DBPF"
4 | description="DBPF Importer"
5 | author="Adrien Jaguenet"
6 | version=""
7 | script="DBPFPlugin.gd"
8 |
--------------------------------------------------------------------------------
/cSTETerrain__SaveAltitudes.gd:
--------------------------------------------------------------------------------
1 | extends DBPFSubfile
2 | class_name cSTETerrain__SaveAltitudes
3 |
4 | var width : int
5 | var height : int
6 | var altitudes : Array
7 |
8 | func _init(index).(index):
9 | pass
10 |
11 | func load(file, dbpf=null):
12 | .load(file, dbpf)
13 | stream.data_array = raw_data
14 | var major = stream.get_16()
15 | print("major: %08x" % major)
16 | print("size: %d" % stream.get_size())
17 | for i in stream.get_size() / 4:
18 | altitudes.append(stream.get_float())
19 |
20 | func set_dimensions(width_, height_):
21 | self.width = width_
22 | self.height = height_
23 |
24 | func get_altitude(x, y):
25 | return altitudes[x * width + y]
26 |
--------------------------------------------------------------------------------
/dev_notes/known_tgis/city - god mode.txt:
--------------------------------------------------------------------------------
1 | Side menu - main: 0x00000000 0x96a006b0 0x69e3d347
2 | Green submenu: 0x00000000 0x96a006b0 0xe9923283
3 | Brown submenu: 0x00000000 0x96a006b0 0xaaa44448
4 |
--------------------------------------------------------------------------------
/ini.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 | class_name INI
3 |
4 | var sections = {}
5 | var file_path
6 |
7 | func _init(path):
8 | file_path = path
9 | var file = File.new()
10 | var err = file.open(file_path, File.READ)
11 | var current_section = ""
12 | if err != OK:
13 | Logger.error("Couldn't load file %s. Error: %s " % [file_path, err] )
14 | return err
15 | while ! file.eof_reached():
16 | var line = file.get_line()
17 | line = line.strip_edges(true, true)
18 | if line.length() == 0:
19 | continue
20 | if line[0] == '#' or line[0] == ';':
21 | continue
22 | if line[0] == '[':
23 | current_section = line.substr(1, line.length() - 2)
24 | sections[current_section] = {}
25 | else:
26 | var key = line.split('=')[0]
27 | var value = line.split('=')[1]
28 | sections[current_section][key] = value
29 |
30 | DebugUtils.print_dict(sections, self)
31 |
32 |
33 | func save_file():
34 | var file = File.new()
35 | file.open(file_path, File.WRITE)
36 | for section in sections.keys():
37 | file.store_line('[' + section + ']')
38 | for line in sections[section].keys():
39 | file.store_line(line + '=' + sections[section][line])
40 |
41 |
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=4
10 |
11 | _global_script_classes=[ {
12 | "base": "DBPFSubfile",
13 | "class": "CURSubfile",
14 | "language": "GDScript",
15 | "path": "res://addons/dbpf/CURSubfile.gd"
16 | }, {
17 | "base": "Node",
18 | "class": "CUR_Entry",
19 | "language": "GDScript",
20 | "path": "res://addons/dbpf/CUR_Entry.gd"
21 | }, {
22 | "base": "Reference",
23 | "class": "DBDF",
24 | "language": "GDScript",
25 | "path": "res://addons/dbpf/DBDF.gd"
26 | }, {
27 | "base": "Node",
28 | "class": "DBDFEntry",
29 | "language": "GDScript",
30 | "path": "res://addons/dbpf/DBDFEntry.gd"
31 | }, {
32 | "base": "Resource",
33 | "class": "DBPF",
34 | "language": "GDScript",
35 | "path": "res://addons/dbpf/DBPF.gd"
36 | }, {
37 | "base": "EditorPlugin",
38 | "class": "DBPFPlugin",
39 | "language": "GDScript",
40 | "path": "res://addons/dbpf/DBPFPlugin.gd"
41 | }, {
42 | "base": "Reference",
43 | "class": "DBPFSubfile",
44 | "language": "GDScript",
45 | "path": "res://addons/dbpf/DBPFSubfile.gd"
46 | }, {
47 | "base": "DBPFSubfile",
48 | "class": "ExemplarSubfile",
49 | "language": "GDScript",
50 | "path": "res://addons/dbpf/ExemplarSubfile.gd"
51 | }, {
52 | "base": "DBPFSubfile",
53 | "class": "FSHSubfile",
54 | "language": "GDScript",
55 | "path": "res://addons/dbpf/FSHSubfile.gd"
56 | }, {
57 | "base": "Node",
58 | "class": "FSH_Entry",
59 | "language": "GDScript",
60 | "path": "res://addons/dbpf/FSH_Entry.gd"
61 | }, {
62 | "base": "Control",
63 | "class": "GZWin",
64 | "language": "GDScript",
65 | "path": "res://addons/dbpf/GZWin.gd"
66 | }, {
67 | "base": "GZWin",
68 | "class": "GZWinBMP",
69 | "language": "GDScript",
70 | "path": "res://addons/dbpf/GZWinBMP.gd"
71 | }, {
72 | "base": "GZWin",
73 | "class": "GZWinBtn",
74 | "language": "GDScript",
75 | "path": "res://addons/dbpf/GZWinBtn.gd"
76 | }, {
77 | "base": "GZWin",
78 | "class": "GZWinFlatRect",
79 | "language": "GDScript",
80 | "path": "res://addons/dbpf/GZWinFlatRect.gd"
81 | }, {
82 | "base": "GZWin",
83 | "class": "GZWinGen",
84 | "language": "GDScript",
85 | "path": "res://addons/dbpf/GZWinGen.gd"
86 | }, {
87 | "base": "GZWin",
88 | "class": "GZWinText",
89 | "language": "GDScript",
90 | "path": "res://addons/dbpf/GZWinText.gd"
91 | }, {
92 | "base": "Node",
93 | "class": "INI",
94 | "language": "GDScript",
95 | "path": "res://ini.gd"
96 | }, {
97 | "base": "DBPFSubfile",
98 | "class": "INISubfile",
99 | "language": "GDScript",
100 | "path": "res://addons/dbpf/INISubfile.gd"
101 | }, {
102 | "base": "DBPFSubfile",
103 | "class": "ImageSubfile",
104 | "language": "GDScript",
105 | "path": "res://addons/dbpf/ImageSubfile.gd"
106 | }, {
107 | "base": "DBPFSubfile",
108 | "class": "LTEXTSubfile",
109 | "language": "GDScript",
110 | "path": "res://addons/dbpf/LTEXTSubfile.gd"
111 | }, {
112 | "base": "Reference",
113 | "class": "NetGraphEdge",
114 | "language": "GDScript",
115 | "path": "res://CityView/ClassDefinitions/NetGraphEdge.gd"
116 | }, {
117 | "base": "Reference",
118 | "class": "NetGraphNode",
119 | "language": "GDScript",
120 | "path": "res://CityView/ClassDefinitions/NetGraphNode.gd"
121 | }, {
122 | "base": "Reference",
123 | "class": "NetTile",
124 | "language": "GDScript",
125 | "path": "res://CityView/ClassDefinitions/NetTile.gd"
126 | }, {
127 | "base": "Reference",
128 | "class": "NetworkGraph",
129 | "language": "GDScript",
130 | "path": "res://CityView/ClassDefinitions/NeworkGraph.gd"
131 | }, {
132 | "base": "DBPFSubfile",
133 | "class": "RULSubfile",
134 | "language": "GDScript",
135 | "path": "res://addons/dbpf/RULSubfile.gd"
136 | }, {
137 | "base": "Area2D",
138 | "class": "RegionCityView",
139 | "language": "GDScript",
140 | "path": "res://RegionUI/RegionCityView.gd"
141 | }, {
142 | "base": "Sprite",
143 | "class": "RegionViewCityThumbnail",
144 | "language": "GDScript",
145 | "path": "res://RegionViewCityThumbnail.gd"
146 | }, {
147 | "base": "DBPFSubfile",
148 | "class": "S3DSubfile",
149 | "language": "GDScript",
150 | "path": "res://addons/dbpf/S3DSubfile.gd"
151 | }, {
152 | "base": "Node",
153 | "class": "S3D_Group",
154 | "language": "GDScript",
155 | "path": "res://addons/dbpf/S3D_Group.gd"
156 | }, {
157 | "base": "DBPFSubfile",
158 | "class": "SC4ReadRegionalCity",
159 | "language": "GDScript",
160 | "path": "res://SC4ReadRegionalCity.gd"
161 | }, {
162 | "base": "DBPFSubfile",
163 | "class": "SC4UISubfile",
164 | "language": "GDScript",
165 | "path": "res://SC4UISubfile.gd"
166 | }, {
167 | "base": "Node",
168 | "class": "SubfileIndex",
169 | "language": "GDScript",
170 | "path": "res://addons/dbpf/SubfileIndex.gd"
171 | }, {
172 | "base": "Node",
173 | "class": "SubfileTGI",
174 | "language": "GDScript",
175 | "path": "res://addons/dbpf/SubfileTGI.gd"
176 | }, {
177 | "base": "Reference",
178 | "class": "TransitTile",
179 | "language": "GDScript",
180 | "path": "res://CityView/ClassDefinitions/TransitTile.gd"
181 | }, {
182 | "base": "DBPFSubfile",
183 | "class": "cSTETerrain__SaveAltitudes",
184 | "language": "GDScript",
185 | "path": "res://cSTETerrain__SaveAltitudes.gd"
186 | } ]
187 | _global_script_class_icons={
188 | "CURSubfile": "",
189 | "CUR_Entry": "",
190 | "DBDF": "",
191 | "DBDFEntry": "",
192 | "DBPF": "",
193 | "DBPFPlugin": "",
194 | "DBPFSubfile": "",
195 | "ExemplarSubfile": "",
196 | "FSHSubfile": "",
197 | "FSH_Entry": "",
198 | "GZWin": "",
199 | "GZWinBMP": "",
200 | "GZWinBtn": "",
201 | "GZWinFlatRect": "",
202 | "GZWinGen": "",
203 | "GZWinText": "",
204 | "INI": "",
205 | "INISubfile": "",
206 | "ImageSubfile": "",
207 | "LTEXTSubfile": "",
208 | "NetGraphEdge": "",
209 | "NetGraphNode": "",
210 | "NetTile": "",
211 | "NetworkGraph": "",
212 | "RULSubfile": "",
213 | "RegionCityView": "",
214 | "RegionViewCityThumbnail": "",
215 | "S3DSubfile": "",
216 | "S3D_Group": "",
217 | "SC4ReadRegionalCity": "",
218 | "SC4UISubfile": "",
219 | "SubfileIndex": "",
220 | "SubfileTGI": "",
221 | "TransitTile": "",
222 | "cSTETerrain__SaveAltitudes": ""
223 | }
224 |
225 | [application]
226 |
227 | config/name="OpenSC4"
228 | run/main_scene="res://BootScreen.tscn"
229 | boot_splash/image="res://splash.png"
230 | config/icon="res://splash.png"
231 |
232 | [autoload]
233 |
234 | Boot="*res://Boot.gd"
235 | Core="*res://Core.gd"
236 | Logger="*res://utils/logger.gd"
237 | DebugUtils="*res://utils/debug_utils.gd"
238 | Utils="*res://utils/Utils.gd"
239 | Player="*res://CityView/Player.gd"
240 |
241 | [display]
242 |
243 | window/size/width=1280
244 | window/size/height=720
245 |
246 | [editor_plugins]
247 |
248 | enabled=PoolStringArray( "res://addons/dbpf/plugin.cfg" )
249 |
250 | [gdnative]
251 |
252 | singletons=[ ]
253 |
254 | [global]
255 |
256 | import=false
257 | scene=false
258 | class=false
259 | debug=false
260 | edit=false
261 |
262 | [input]
263 |
264 | camera_up={
265 | "deadzone": 0.5,
266 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
267 | ]
268 | }
269 | camera_down={
270 | "deadzone": 0.5,
271 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
272 | ]
273 | }
274 | camera_left={
275 | "deadzone": 0.5,
276 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
277 | ]
278 | }
279 | camera_right={
280 | "deadzone": 0.5,
281 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
282 | ]
283 | }
284 | camera_right_click={
285 | "deadzone": 0.5,
286 | "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null)
287 | ]
288 | }
289 |
290 | [network]
291 |
292 | limits/debugger_stdout/max_chars_per_second=20000
293 | limits/debugger_stdout/max_messages_per_frame=100
294 |
295 | [physics]
296 |
297 | common/enable_pause_aware_picking=true
298 |
299 | [rendering]
300 |
301 | 2d/snapping/use_gpu_pixel_snap=true
302 | threads/thread_model=2
303 | vram_compression/import_etc=true
304 | vram_compression/import_etc2=false
305 | quality/shadow_atlas/cubemap_size=1024
306 | quality/shadows/filter_mode=2
307 | quality/filters/use_fxaa=true
308 |
--------------------------------------------------------------------------------
/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenSC4-org/OpenSC4/fdc90c33e6e012b8c825f20ee10c51f55796df39/splash.png
--------------------------------------------------------------------------------
/splash.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/splash.png-929ed8a00b89ba36c51789452f874c77.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://splash.png"
13 | dest_files=[ "res://.import/splash.png-929ed8a00b89ba36c51789452f874c77.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/utils/debug_ui/loading_icon_spinning.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="AnimatedTexture" load_steps=9 format=2]
2 |
3 | [ext_resource path="res://utils/debug_ui/load5.png" type="Texture" id=1]
4 | [ext_resource path="res://utils/debug_ui/load0.png" type="Texture" id=2]
5 | [ext_resource path="res://utils/debug_ui/load3.png" type="Texture" id=3]
6 | [ext_resource path="res://utils/debug_ui/load6.png" type="Texture" id=4]
7 | [ext_resource path="res://utils/debug_ui/load7.png" type="Texture" id=5]
8 | [ext_resource path="res://utils/debug_ui/load1.png" type="Texture" id=6]
9 | [ext_resource path="res://utils/debug_ui/load2.png" type="Texture" id=7]
10 | [ext_resource path="res://utils/debug_ui/load4.png" type="Texture" id=8]
11 |
12 | [resource]
13 | flags = 4
14 | frames = 8
15 | fps = 30.0
16 | frame_0/texture = ExtResource( 2 )
17 | frame_1/texture = ExtResource( 6 )
18 | frame_1/delay_sec = 0.0
19 | frame_2/texture = ExtResource( 7 )
20 | frame_2/delay_sec = 0.0
21 | frame_3/texture = ExtResource( 3 )
22 | frame_3/delay_sec = 0.0
23 | frame_4/texture = ExtResource( 8 )
24 | frame_4/delay_sec = 0.0
25 | frame_5/texture = ExtResource( 1 )
26 | frame_5/delay_sec = 0.0
27 | frame_6/texture = ExtResource( 4 )
28 | frame_6/delay_sec = 0.0
29 | frame_7/texture = ExtResource( 5 )
30 | frame_7/delay_sec = 0.0
31 |
--------------------------------------------------------------------------------
/utils/debug_utils.gd:
--------------------------------------------------------------------------------
1 | extends Node
2 |
3 | const _PRINT_DEBUG: bool = true
4 |
5 |
6 | func print_dict(dict, node:Node):
7 | if _PRINT_DEBUG:
8 | for key in dict:
9 | print(key)
10 | for key2 in dict[key]:
11 | print("\t" + key2 + " = " + dict[key][key2])
12 |
--------------------------------------------------------------------------------