Hacking Cities:Skylines

https://github.com/yasushisakai/CitiesSkylinesTestMod

api

curl 34.207.233.31/api/cities/list
name“:”Test City d2d1db3b-a504-4d09-afd1-dda7be952e72 Black Woods 18.27.123.81:9000
curl 34.207.233.31/api/city/info/Test%20City | jq .
{  
simrunning false,
elapsed 1215210.9375,
population 5
}  
curl -d '{"name":"toggle_sim"}' -H "Content-Type: application/json" 34.207.233.31/api/city/command/Test%20City
response: OK
curl -d '{"name":"upload_screen_shot"}' -H "Content-Type: application/json" 34.207.233.31/api/city/command/Test%20City
response: OK

Mod 開発準備

下記が、親切に教えてくれてそう… https://community.simtropolis.com/forums/topic/73404-modding-tutorial-0-your-first-mod/

だけど、いきなり古すぎて対応していない箇所があったので、半信半疑でやったほうがよさそう。結局リバースエンジニアリングが出てくるのは避けられなそう。

https://skylines.paradoxwikis.com/Modding の公式? の方がまだアップデートされている模様

fbx の dump ができそう https://steamcommunity.com/sharedfiles/filedetails/?id=2434651215&searchtext=ModTools

ModTools をいれたほうが良い

ゲーム内のアセットの一覧や情報を取得するために便利な ModTools という Mod があるので、それを入れる。 Debug.Log もここから見えるので、必須です。

Visual Studio で開発…

Create a new project in VS2017:

Select File > New > Project On the left, select Templates > Visual C# > Windows Classic Desktop Select “Class Library (.NET Framework)” In the top, select “.NET Framework 3.5” in the dropdown menu (Important!) In the bottom, enter “SecondMod” as the name and solution name. and choose a location for your project files (e.g. your desktop) Press OK

https://skylines.paradoxwikis.com/User_path によると、 windows だと %LOCALAPPDATA%/Collossal Order/Cities_Skyline/Addons/Mods の中に

- Mods
    - TestMod
        - Source
            - Test.cs

に書けば、ゲームがロード時にコンパイルしてくれるらしい。 linux の場合、 ~/.steam

/home/yasushi/.local/share/Steam/steamapps/common/Cities_Skylines/Files/Mods
/home/yasushi/.local/share/Colossal Order/Cities_Skylines/Addons/Mods

のどちらかにありそうなもの。チュートリアル的には、後者のようなものだけど、未検証。コード補完の dll のリンクさえ出来れば、開発できそう。

でも結局、なんかいろいろ大変そうだったのでおとなしく、windows + visual studio で開発することがいいような気もする。くやしいけど。

あとのメモとして、Unity のバージョンは 5 らしい。最新の Unity Scripting API でつかえないものもあるかもしれない。

https://github.com/yasushisakai/CitiesSkylinesTestMod でつくり初めたけど、まあ、バッドノウハウばっかり笑

以下やれるといいこと:

情報の反映

人を追加したり、建物を追加したり。ケント的には混合用途の建物を追加できないかともいっていた。でも、まあ、これはそんなに心配していない。

SimulationManager.instance.SimulationPaused = !SimulationManager.instance.SimulationPaused

とすれば、シミュレーションがとまったり、はしったりする。

自動起動(と終了)、Mod のビルド

開発そのものは、vscode から、remote で windows 機のファイルをいじる、(コード補完とかも remote 側が使えるし、そもそも c#を mac で開発する気になれない。)

とにかく起動がおそく、再起動にいろいろハードルがあり、これを自動化しないと全然だめ。

以下を組みあわせれば、Mod のビルドから Cities の起動と強制終了ができる。マウスのエミュレーションがいらなくて助かる。

強制終了

taskkill /f /t /im "Cities.exe"

起動

ローカルなら以下でいいんだけど、

start /MIN /D"C:\Program Files (x86)\Steam\steamapps\common\Cities_Skylines" Cities.exe

ssh 経由だとだめらしく、 PSExec というコマンド経由で起動するらしい。これは PSTools というのを DL して、PATH に追加しておく。

その後さらに、

Ok, I’ve found the answer. There is a GPO on the domain that needs to be added. Computer Config > Windows settings > Security Settings > Local Policies > User Rights Assignment: Make sure “allow log on locally” and “allow log on through remote desktop services” both are enabled with your admin account listed. Once I did that, all PowerShell scripts on a shared folder I had could be successfully executed on all systems with PSExec.

だったり、レジストリをいじったり色々したらなんか出来た。

結局以下にしないとだめ。

psexec \\localhost -i "C:\Program Files (x86)\Steam\steamapps\common\Cities_Skylines\Cities.exe"

Paradox launcher をスキップする

その上で、Paradox のランチャーをスキップしたい時は下記のプログラムをいれる。

https://github.com/shusaura85/notparadoxlauncher

最後に遊んだファイルを自動ロードするオプションがあるのでそれをつける。

… Pascal…

もしかすると、Paradox Launcher もオプションつければ、自動的に最後のセーブをロードしてくれるのかもしれない。

ビルド

...Microsoft Visual Studio\2017\Community\MSBuild\15.0\MSBuild.exe" {Path to Mod}

複数インスタンスによる中継

https://citiesskylinemultiplayer.com

をすれば、複数インスタンスを走らせることができそう。操作しないまでも、町の様子を中継するのは出来そう。

API 深掘り

公式の API にはほとんど情報が載っていないので、ModTools と ILSpy を駆使して、リバースエンジニアリング。ILSpy でみえるものは、Decompile できるので、それをながめる。

ILSpy は、

C:/Program Files (x86)/Steam/steamapps/common/Cities_Skylines/Cities_Data/Managed

に入っている dll ファイルが狙い。

Screenshot

os 側でとったりしてもよかったんだけど、mod 側で出来た。

SnapshotTool.Snapshot(...) の内容をパクったらとりあえず、取れた。

最新の Unity だと、関数が用意されていたり、なにより、 async/await 1が使えたりして、うらやましいけど、文句いわない。

渋滞情報だったり、表示を替えたりしている状態で mod が走れば、そのまま映してくれる。

DistrictManager

色んなデータがここからとれそう。 DistrictManager.instance.m_districts.m_buffer[0].m_populationData.m_finalCount で人口がとれた。

この m_buffer[*] の型が District.cs らしい、この中にももちろん沢山型があるんだけど、その中でクラス変数で関係ありそうなのは…

選定する必要ありだけど…

DistrictAgeData m_adultData;
DistrictAgeData m_ageAtDeathData;
DistrictAgeData m_birthData;
DistrictAgeData m_childData;
DistrictAgeData m_childHealthData;
DistrictAgeData m_childSickData;
DistrictAgeData m_deadSeniorsData;
DistrictAgeData m_deathData;
DistrictAgeData m_education1Data; // 1-3
DistrictAgeData m_elementaryEligibleData;
DistrictAgeData m_highschoolEligibleData;
DistrictAgeData m_libraryVisitorData;
DistrictAgeData m_seniorData;
DistrictAgeData m_seniorHealthData;
DistrictAgeData m_seniorSickData;
DistrictAgeData m_student1Data; // 1-3
DistrictAgeData m_teenData;
DistrictAgeData m_universityEligibleData;
DistrictAgeData m_youngData;
DistrictConsumptionData m_commercialConsumption;
DistrictConsumptionData m_industrialConsumption;
DistrictConsumptionData m_officeConsumption;
DistrictConsumptionData m_playerConsumption;
DistrictConsumptionData m_residentialConsumption;
DistrictEducationData m_educated0Data; // 0-3
DistrictGroundData m_groundData;
DistrictPopulationData m_populationData; // ****
DistrictPrivateData m_commercialData;
DistrictPrivateData m_industrialData;
DistrictPrivateData m_officeData;
DistrictPrivateData m_playerData;
DistrictPrivateData m_residentialData;
DistrictPrivateData m_visitorData;
DistrictProductionData m_productionData;
DistrictResourceData m_exportData;
DistrictResourceData m_importData;
DistrictSubCultureData m_gangstaData;
DistrictSubCultureData m_hippieData;
DistrictSubCultureData m_hipsterData;
DistrictSubCultureData m_redneckData;
DistrictTouristData m_tourist1Data;
DistrictTouristData m_tourist2Data;
DistrictTouristData m_tourist3Data;
DistrictUsageData m_usageData;
byte m_lastAverageLifespan;
byte m_finalHappiness;
byte m_finalCrimeRate;

メソッドでいうと…

void SimulationStep(byte districtID)
void AddXXXData(...)
int GetAverageLifespan()
int GetCremateCapacity()
int GetCriminalAmount()
int GetCriminalCapacity()
int GetDeadAmount()
int GetDeadCapacity()
int GetDeadCount()
int GetEducation1Capacity() // 1,2,3
int GetEducation1Need() // 1,2,3
int GetEducation1Rate() // 1,2,3
int GetElectricityCapacity()
int GetElectricityConsumption()
int GetExportAmount()
int GetExtraCriminals()
int GetGarbageAccumulation()
int GetGarbageAmount()
int GetGarbageCapacity()
int GetGarbagePiles()
int GetGroundPollution()
int GetHealCapacity()
int GetHeatingCapacity()
int GetHeatingConsumption()
int GetImportAmount()
int GetIncinerationCapacity()
int GetIncomeAccumulation()
int GetLandValue()
int GetLibraryCapacity()
int GetLibraryVisitorCount()
int GetSewageAccumulation()
int GetSewageCapacity()
int GetShelterCitizenCapacity()
int GetShelterCitizenNumber()
int GetSickCount()
int GetUnemployment()
int GetWaterCapacity()
int GetWaterConsumption()
int GetWaterPollution()
int GetWaterStorageAmount()
int GetWaterStorageCapacity()
int GetWorkerCount()
int GetWorkplaceCount()

あたりかな?

Unity 上で TCP Server をたてて外部から操作する

TCP のうけつけもひとつのスレッドだし、そのままそのスレッドで client 捌いてるから、大量のレスはうけれない。(せめて、accept 後は、別スレッドたてる?)

using UnityEngine;
using System.Threading;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace CitiesSkylinesTestMod
{

    public class TCPServer : MonoBehaviour
    {

        private Thread listeningThread;
        private TcpListener listener;
        private int port = 44445;
        private TcpClient client;

        void Start()
        {
            listeningThread = new Thread(new ThreadStart(AcceptClientLoop));
            listeningThread.IsBackground = true;
            listeningThread.Start();
        }
        private void AcceptClientLoop()
        {
            try
            {
                listener = new TcpListener(IPAddress.Parse("127.0.0.1"), port);
                // listener = new TcpListener(IPAddress.Any, port);
                listener.Start();
                Debug.Log("Listener started");
                byte[] buffer = new byte[1024];
                while (true)
                {
                    using (client = listener.AcceptTcpClient())
                    {
                        using (NetworkStream stream = client.GetStream())
                        {
                            int len;
                            while ((len = stream.Read(buffer, 0, buffer.Length)) != 0)
                            {
                                var incoming = new byte[len];
                                Array.Copy(buffer, 0, incoming, 0, len);
                                string mes = Encoding.ASCII.GetString(incoming);

                                if (mes == "toggle sim")
                                {
                                    SimulationManager.instance.SimulationPaused = !SimulationManager.instance.SimulationPaused;
                                    SendResponse(stream, "ok");
                                }

                                Debug.Log("client mes: " + mes);
                            }
                        }
                        client.Close();
                        client = null;
                    }
                }
            }
            catch (SocketException e)
            {
                Debug.Log(e.ToString());
            }
        }

        private void SendResponse(NetworkStream stream, string message)
        {
            try
            {
                if (stream.CanWrite)
                {
                    byte[] bytesToSend = Encoding.ASCII.GetBytes(message);
                    stream.Write(bytesToSend, 0, bytesToSend.Length);
                    Debug.Log("sent message: " + message);
                }
            }
            catch (SocketException e)
            {
                Debug.Log(e.ToString());
            }
        }
    }
}

windows に nginx 初めていれたかも。 nginx.conf

stream {
  server {
    listen 9000 # open this port through windows firewall config.
    proxy_pass 127.0.0.1:44445;
  }
}

ためしに python でおくってみる。

import socket

ip = IP_ADDRESS
port = 44445
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print('sending message')
s.sendall(b'toggle sim')
mes = s.recv(1024)
s.close()
print(str(mes))

Camera 諸々

MainCamera > CameraController > mtargetangle mtargetposition (x, y, z)

mcurrentsize() 40-4000

rhino で スケッチ

import rhinoscriptsyntax as rs
import math;

path = '/Users/yasushi/Dropbox/temp/blocks.csv';
f = open(path, 'r')
lines = f.readlines()
# print(lines)
r = 5;

all = rs.AllObjects();

rs.DeleteObjects(all);

for line in lines:
    [id, xpos, ypos, zpos, angle, xi, zi, zone] = [v.strip() for v in line.split(',')]
    [xpos, ypos, zpos, xi, zi, angle] = [float(v) for v in [xpos, ypos, zpos, xi, zi, angle]]

    # rhino x is z in cs
    x =  zpos - zi * r * 2
    # rhino y is x in cs
    y =  xpos + xi * r * 2
    # z -> y
    z = 0.0

    print(zpos, xpos)
    rs.AddPoint((zpos, xpos))

    circle = rs.AddCircle([x, y, z], r);
    deg = angle * 180.0 / math.pi;
    # rs.RotateObject(circle, [zpos, xpos, 0.0], deg);
    rs.ObjectName(circle,id + "-" + zone + ':' + str(xi) + ',' + str(zi));

    if zi == 0.0 and xi == 0.0:
        rs.ObjectColor(circle, (255,0,0))
    elif zi == 0.0:
        rs.ObjectColor(circle, (0,0,255)) # blue
    elif xi == 0.0:
        rs.ObjectColor(circle, (0,255,0)) # green

未整理だけど….

import rhinoscriptsyntax as rs
import math;
import json;

path = '/Users/yasushi/Dropbox/temp/blocks.json';
f = open(path, 'r')
data = json.load(f);
r = 5;

all = rs.AllObjects();

rs.DeleteObjects(all);

def tx_coord(coord):
    new = [0]*3;
    new[0] = coord[0];
    new[1] = coord[2];
    new[2] = coord[1];
    return new

# nodes
nodes = {}
for n in data['nodes']:
    nodes[n['id']] = n['pos']
    # render them
    rs.AddCircle(tx_coord(n['pos']),3);

# blocks
blocks = {}

for b in data['blocks']:
    if len(b['cells']) != 0 :
        blocks[b['id']] = b

# naive vector calculations
def rot_vec_2d(v, rad):
    result = [0]*2;
    result[0] = v[0] * math.cos(rad) - v[1] * math.sin(rad)
    result[1] = v[0] * math.sin(rad) + v[1] * math.cos(rad)
    return result

def norm(v):
    f = math.sqrt(v[0]*v[0] + v[1]*v[1])
    return [e/f for e in v]

def mul(v, s):
    return [e*s for e in v]

def add(v, o):
    return [e + o[i] for (i, e) in enumerate(v)]

# edges
for e in data['edges']:

    sn = nodes[e['nodes'][0]]
    en = nodes[e['nodes'][1]]
    rs.AddCircle(tx_coord(sn), 2);
    rs.AddLine(tx_coord(sn), tx_coord(en))
    d = [e['sd'][0], e['sd'][2]]
    rot = rot_vec_2d([e['sd'][0], e['sd'][2]], math.pi/2)

    if e['sl'] != 0 and blocks.has_key(e['sl']):
        cells = blocks[e['sl']]['cells']
        print(cells)
        for c in cells:
            tx = add(mul(rot, 4 * 2 * (c['x']+1)), mul(d, 4 *2 * c['z']));
            center = [sn[0] + tx[0], sn[1], sn[2] + tx[1]]
            rs.AddCircle(tx_coord(center), 4);

    if e['sr'] != 0 and blocks.has_key(e['sr']):
        cells = blocks[e['sr']]['cells']
        print(cells)
        for c in cells:
            tx = add(mul(rot, - 4 * 2 * (c['x']+1)), mul(d, 4 * 2 * (c['z'])));
            center = [sn[0] + tx[0], sn[1], sn[2] + tx[1]]
            rs.AddCircle(tx_coord(center), 4);

Camera

Angles

mtargetAngle = x, y,

Footnotes:

1

仕様としては、Javascript のそれと近いのかも。

Date: 2022-07-26 Tue 16:59