OmniOS와 OpenTTD JGRPP에 대해
OmniOS는 Solaris 기반의 오픈소스 운영체제다. Oracle Solaris에서 파생된 illumos 프로젝트를 기반으로 하며, 서버 환경에서의 안정성과 성능에 중점을 둔 Unix 계열 OS다. ZFS 파일시스템과 DTrace 등 Solaris의 강력한 기능들을 그대로 제공하면서도 완전히 오픈소스로 유지되고 있어, 엔터프라이즈 환경에서 높은 평가를 받고 있다.
OpenTTD JGRPP(JGR’s Patchpack)는 클래식 게임 Transport Tycoon Deluxe의 오픈소스 재구현인 OpenTTD에 다양한 기능 개선과 패치를 적용한 확장 버전이다. JGR(Jonathan G Rennison)이 개발하는 이 패치팩은 원본 OpenTTD에 없는 고급 기능들을 제공한다 – 프로그래밍 가능한 신호 시스템, 확장된 차량 관리 기능, 개선된 철도 네트워크 등이 그것이다. 특히 Dedicated 서버는 GUI 없이 서버 전용으로 실행되어 멀티플레이어 게임 호스팅에 최적화되어 있다.
일반적으로 Windows나 Linux에서 주로 실행되는 OpenTTD JGRPP Dedicated 서버를 OmniOS에서 빌드하려면 몇 가지 플랫폼별 호환성 문제를 해결해야 한다. 복잡하지는 않지만 Solaris 특유의 시스템 차이점을 이해하고 적절한 패치를 적용하는 과정이 필요하다.
이번 포스트에서는 빌드 명령어와 필요한 수정사항들을 간단하게 정리했다.
요구 사항
- OmniOS r151048 LTS (이 글을 작성하는 시점에서 최신 LTS 릴리즈)
- sudo 권한 혹은 관리자 권한
- 인터넷 연결
OS 정보
~$ uname -a
SunOS jgrpp01 5.11 omnios-r151054-6ad70ba62c i86pc i386 i86pc
~$ cat /etc/release
OmniOS v11 r151054
Copyright (c) 2012-2017 OmniTI Computer Consulting, Inc.
Copyright (c) 2017-2025 OmniOS Community Edition (OmniOSce) Association.
All rights reserved. Use is subject to licence terms.
~$ cat /etc/os-release
NAME="OmniOS"
PRETTY_NAME="OmniOS Community Edition v11 r151054"
CPE_NAME="cpe:/o:omniosce:omnios:11:151054:0"
ID=omnios
VERSION=r151054
VERSION_ID=r151054
BUILD_ID=151054.0.2025.05.02
HOME_URL="https://omnios.org/"
SUPPORT_URL="https://omnios.org/"
BUG_REPORT_URL="https://github.com/omniosorg/omnios-build/issues/new"
1. 필수 패키지 설치
sudo pkg update
sudo pkg install cmake gcc14 libpng git bison flex
2. 의존성 라이브러리 빌드
작업 디렉터리 생성
mkdir jgrpp && cd jgrpp
LZO 빌드
wget http://www.oberhumer.com/opensource/lzo/download/lzo-2.10.tar.gz
tar xvzf lzo-2.10.tar.gz
cd lzo-2.10
./configure --prefix=/usr
make -j $(n=$(psrinfo -t); [ "$n" -le 1 ] && echo 1 || expr $n - 1) && sudo make install
cd ..
grfcodec 빌드
wget https://sourceforge.net/projects/boost/files/boost/1.88.0/boost_1_88_0.tar.gz/download -O boost_1_88_0.tar.gz
tar xvzf boost_1_88_0.tar.gz
git clone https://github.com/OpenTTD/grfcodec.git
cd grfcodec && mkdir build && cd build && cmake .. -DBOOST_ROOT=../boost_1_88_0 -DBoost_NO_SYSTEM_PATHS=ON
make -j $(n=$(psrinfo -t); [ "$n" -le 1 ] && echo 1 || expr $n - 1) && sudo make install
cd .. && cd ..
Doxygen 빌드
wget https://github.com/doxygen/doxygen/releases/download/Release_1_13_2/doxygen-1.13.2.src.tar.gz
tar xvzf doxygen-1.13.2.src.tar.gz && cd doxygen-1.13.2
mkdir build && cd build && cmake .. -DIconv_LIBRARY=/lib/64/libc.so -DIconv_INCLUDE_DIR=/usr/include -DCMAKE_C_FLAGS="-Wno-incompatible-pointer-types"
make -j $(n=$(psrinfo -t); [ "$n" -le 1 ] && echo 1 || expr $n - 1) && sudo make install
cd .. && cd ..
3. OpenTTD JGRPP 소스 코드 준비
git clone https://github.com/JGRennison/OpenTTD-patches.git
cd OpenTTD-patches
git fetch --tags
git checkout jgrpp-0.65.3
※ jgrpp-0.65.3은 글을 작성하는 시점에서 가장 최신 버전임
버전 체크 → OpenTTD JGRPP
4. OmniOS 호환성을 위한 소스 코드 수정 (OpenTTD-patches 디렉터리 기준)
CMakeLists.txt
target_link_libraries(${OPENTTD_LIB}
openttd::languages
openttd::settings
openttd::script_api
Threads::Threads
)
↓
target_link_libraries(${OPENTTD_LIB}
openttd::languages
openttd::settings
openttd::script_api
Threads::Threads
-lsocket
)
src/core/random_func.cpp
#elif defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__) || defined(__OpenBSD__)
arc4random_buf(buf.data(), buf.size());
return;
↓
#elif defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__sun)
arc4random_buf(buf.data(), buf.size());
return;
src/network/core/os_abstraction.cpp
bool SetReusePort(SOCKET d)
{
#ifdef _WIN32
/* Windows has no SO_REUSEPORT, but for our usecases SO_REUSEADDR does the same job. */
int reuse_port = 1;
return setsockopt(d, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse_port, sizeof(reuse_port)) == 0;
#else
int reuse_port = 1;
return setsockopt(d, SOL_SOCKET, SO_REUSEPORT, &reuse_port, sizeof(reuse_port)) == 0;
#endif
}
↓
bool SetReusePort(SOCKET d)
{
#ifdef _WIN32
/* Windows has no SO_REUSEPORT, but for our usecases SO_REUSEADDR does the same job. */
int reuse_port = 1;
return setsockopt(d, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse_port, sizeof(reuse_port)) == 0;
#else
int reuse_port = 1;
return setsockopt(d, SOL_SOCKET, SO_REUSEADDR, &reuse_port, sizeof(reuse_port)) == 0;
#endif
}
src/os/unix/crashlog_unix.cpp
#include <errno.h>
#include <signal.h>
#include <sys/utsname.h>
#include <setjmp.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
↓
#include <errno.h>
#include <signal.h>
#include <sys/utsname.h>
#include <setjmp.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <ucontext.h>
#if defined(__x86_64__)
const gregset_t &gregs = ucontext->uc_mcontext.gregs;
buffer.format(
"Registers:\n"
" rax: 0x{:016X} rbx: 0x{:016X} rcx: 0x{:016X} rdx: 0x{:016X}\n"
" rsi: 0x{:016X} rdi: 0x{:016X} rbp: 0x{:016X} rsp: 0x{:016X}\n"
" r8: 0x{:016X} r9: 0x{:016X} r10: 0x{:016X} r11: 0x{:016X}\n"
" r12: 0x{:016X} r13: 0x{:016X} r14: 0x{:016X} r15: 0x{:016X}\n"
" rip: 0x{:016X} eflags: 0x{:08X}, err: 0x{:X}\n\n",
gregs[REG_RAX],
gregs[REG_RBX],
gregs[REG_RCX],
gregs[REG_RDX],
gregs[REG_RSI],
gregs[REG_RDI],
gregs[REG_RBP],
gregs[REG_RSP],
gregs[REG_R8],
gregs[REG_R9],
gregs[REG_R10],
gregs[REG_R11],
gregs[REG_R12],
gregs[REG_R13],
gregs[REG_R14],
gregs[REG_R15],
gregs[REG_RIP],
gregs[REG_EFL],
gregs[REG_ERR]
);
#elif defined(__i386)
const gregset_t &gregs = ucontext->uc_mcontext.gregs;
buffer.format(
"Registers:\n"
" eax: 0x{:08X} ebx: 0x{:08X} ecx: 0x{:08X} edx: 0x{:08X}\n"
" esi: 0x{:08X} edi: 0x{:08X} ebp: 0x{:08X} esp: 0x{:08X}\n"
" eip: 0x{:08X} eflags: 0x{:08X}, err: 0x{:X}\n\n",
gregs[REG_EAX],
gregs[REG_EBX],
gregs[REG_ECX],
gregs[REG_EDX],
gregs[REG_ESI],
gregs[REG_EDI],
gregs[REG_EBP],
gregs[REG_ESP],
gregs[REG_EIP],
gregs[REG_EFL],
gregs[REG_ERR]
);
#endif
↓
#if defined(__x86_64__)
const gregset_t &gregs = ucontext->uc_mcontext.gregs;
buffer.format(
"Registers:\n"
" rax: 0x{:016X} rbx: 0x{:016X} rcx: 0x{:016X} rdx: 0x{:016X}\n"
" rsi: 0x{:016X} rdi: 0x{:016X} rbp: 0x{:016X} rsp: 0x{:016X}\n"
" r8: 0x{:016X} r9: 0x{:016X} r10: 0x{:016X} r11: 0x{:016X}\n"
" r12: 0x{:016X} r13: 0x{:016X} r14: 0x{:016X} r15: 0x{:016X}\n"
" rip: 0x{:016X} eflags: 0x{:08X}, err: 0x{:X}\n\n",
gregs[REG_RAX],
gregs[REG_RBX],
gregs[REG_RCX],
gregs[REG_RDX],
gregs[REG_RSI],
gregs[REG_RDI],
gregs[REG_RBP],
gregs[REG_RSP],
gregs[REG_R8],
gregs[REG_R9],
gregs[REG_R10],
gregs[REG_R11],
gregs[REG_R12],
gregs[REG_R13],
gregs[REG_R14],
gregs[REG_R15],
gregs[REG_RIP],
gregs[REG_RFL],
gregs[REG_ERR]
);
#elif defined(__i386)
const gregset_t &gregs = ucontext->uc_mcontext.gregs;
buffer.format(
"Registers:\n"
" eax: 0x{:08X} ebx: 0x{:08X} ecx: 0x{:08X} edx: 0x{:08X}\n"
" esi: 0x{:08X} edi: 0x{:08X} ebp: 0x{:08X} esp: 0x{:08X}\n"
" eip: 0x{:08X} eflags: 0x{:08X}, err: 0x{:X}\n\n",
gregs[REG_EAX],
gregs[REG_EBX],
gregs[REG_ECX],
gregs[REG_EDX],
gregs[REG_ESI],
gregs[REG_EDI],
gregs[REG_EBP],
gregs[REG_ESP],
gregs[REG_EIP],
gregs[REG_RFL],
gregs[REG_ERR]
);
#endif
src/string_func.h
/* strcasestr is available for _GNU_SOURCE, BSD and some Apple */
#if defined(_GNU_SOURCE) || (defined(__BSD_VISIBLE) && __BSD_VISIBLE) || (defined(__APPLE__) && (!defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE))) || defined(_NETBSD_SOURCE)
# undef DEFINE_STRCASESTR
#else
# define DEFINE_STRCASESTR
char *strcasestr(const char *haystack, const char *needle);
#endif /* strcasestr is available */
↓
/* strcasestr is available for _GNU_SOURCE, BSD and some Apple */
#if defined(_GNU_SOURCE) || (defined(__BSD_VISIBLE) && __BSD_VISIBLE) || (defined(__APPLE__) && (!defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE))) || defined(_NETBSD_SOURCE) || defined(__sun)
# undef DEFINE_STRCASESTR
#else
# define DEFINE_STRCASESTR
char *strcasestr(const char *haystack, const char *needle);
#endif /* strcasestr is available */
src/3rdparty/fmt/ranges.h
template <typename T, typename C> class is_tuple_formattable_<T, C, true> {
template <std::size_t... Is>
static auto check2(index_sequence<Is...>,
integer_sequence<bool, (Is == Is)...>) -> std::true_type;
static auto check2(...) -> std::false_type;
template <std::size_t... Is>
static auto check(index_sequence<Is...>) -> decltype(check2(
index_sequence<Is...>{},
integer_sequence<bool,
(is_formattable<typename std::tuple_element<Is, T>::type,
C>::value)...>{}));
public:
static constexpr const bool value =
decltype(check(tuple_index_sequence<T>{}))::value;
};
↓
template <typename T, typename C> class is_tuple_formattable_<T, C, true> {
template <std::size_t... Is>
static auto check2(index_sequence<Is...>,
integer_sequence<bool, (true ? static_cast<void>(Is), true : false)...>) -> std::true_type;
static auto check2(...) -> std::false_type;
template <std::size_t... Is>
static auto check(index_sequence<Is...>) -> decltype(check2(
index_sequence<Is...>{},
integer_sequence<bool,
(is_formattable<typename std::tuple_element<Is, T>::type,
C>::value)...>{}));
public:
static constexpr const bool value =
decltype(check(tuple_index_sequence<T>{}))::value;
};
src/schdispatch_gui.cpp
void CcAdjustSchDispatchSlot(const CommandCost &result, VehicleID veh, uint32_t schedule_index, uint32_t offset, int32_t adjustment)
{
if (!result.Succeeded() || !result.HasResultData()) return;
SchdispatchWindow *w = dynamic_cast<SchdispatchWindow *>(FindWindowById(WC_SCHDISPATCH_SLOTS, veh));
if (w != nullptr && w->schedule_index == schedule_index && w->selected_slot == offset) {
w->selected_slot = result.GetResultData();
}
}
↓
void CcAdjustSchDispatchSlot(const CommandCost &result, VehicleID veh, uint32_t schedule_index, uint32_t offset, int32_t adjustment)
{
if (!result.Succeeded() || !result.HasResultData()) return;
SchdispatchWindow *w = dynamic_cast<SchdispatchWindow *>(FindWindowById(WC_SCHDISPATCH_SLOTS, veh));
if (w != nullptr && w->schedule_index >= 0 && static_cast<uint32_t>(w->schedule_index) == schedule_index && w->selected_slot == offset) {
w->selected_slot = result.GetResultData();
}
}
src/rev.cpp.in
/**
* The text version of OpenTTD's revision.
* This will be either
* - "<tag>", like "<major>.<minor>.<build>[-RC<rc>]",
* - "<commitdate>-g<shorthash><modified>" in "master",
* - "<commitdate>-<branch>-g<shorthash><modified>" in other branches, or
* - "norev0000", if the version is unknown.
*
* The major, minor and build are the numbers that describe releases of
* OpenTTD (like 0.5.3). "-RC" is used to flag release candidates.
*
* <modified> shows a "M", if the binary is made from modified source code.
*/
const char _openttd_revision[] = "${REV_VERSION}";
↓
/**
* The text version of OpenTTD's revision.
* This will be either
* - "<tag>", like "<major>.<minor>.<build>[-RC<rc>]",
* - "<commitdate>-g<shorthash><modified>" in "master",
* - "<commitdate>-<branch>-g<shorthash><modified>" in other branches, or
* - "norev0000", if the version is unknown.
*
* The major, minor and build are the numbers that describe releases of
* OpenTTD (like 0.5.3). "-RC" is used to flag release candidates.
*
* <modified> shows a "M", if the binary is made from modified source code.
*/
const char _openttd_revision[] = "jgrpp-0.65.3";
5. 빌드 실행
mkdir build && cd build
export LD_LIBRARY_PATH=/opt/ooce/lib/amd64:$LD_LIBRARY_PATH
CXXFLAGS="-Wno-uninitialized -Wno-maybe-uninitialized" cmake .. -DOPTION_DEDICATED=true
make -j $(n=$(psrinfo -t); [ "$n" -le 1 ] && echo 1 || expr $n - 1)
6. 필수 에셋 설치
wget https://cdn.openttd.org/opengfx-releases/7.1/opengfx-7.1-all.zip
unzip opengfx-7.1-all.zip
mv opengfx-7.1.tar baseset/
7. 실행
./openttd -D