├── .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://dcbadge.vercel.app/api/server/53yN9BjA54)](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 | --------------------------------------------------------------------------------