2024/02/21

ODBC

ODBC(Open Database Connectivity)提供了一組標準的 API 來訪問資料庫管理系統(DBMS)。 這些 API 利用 SQL 來完成其大部分任務。 ODBC 是針對 C 的 API,不過很多的程式語言都有相關的 bindings。如果想使用 C 來寫 ODBC 程式, 可以從 ODBC from C Tutorial Part 1 開始學習如何撰寫。

The ODBC architecture has four components:

  • Application Performs processing and calls ODBC functions to submit SQL statements and retrieve results.

  • Driver Manager Loads and unloads drivers on behalf of an application. Processes ODBC function calls or passes them to a driver.

  • Driver Processes ODBC function calls, submits SQL requests to a specific data source, and returns results to the application. If necessary, the driver modifies an application's request so that the request conforms to syntax supported by the associated DBMS.

  • Data Source Consists of the data the user wants to access and its associated operating system, DBMS, and network platform (if any) used to access the DBMS.

In ODBC there are four main handle types and you will need to know at least three to do anything useful:

  • SQLHENV - environment handle

    This is the first handle you will need as everything else is effectively in the environment. Once you have an environment handle you can define the version of ODBC you require, enable connection pooling and allocate connection handles with SQLSetEnvAttr and SQLAllocHandle.

  • SQLHDBC - connection handle

    You need one connection handle for each data source you are going to connect to. Like environment handles, connection handles have attributes which you can retrieve and set with SQLSetConnectAttr and SQLGetConnectAttr.

  • SQLHSTMT - statement handle

    Once you have a connection handle and have connected to a data source you allocate statement handles to execute SQL or retrieve meta data. As with the other handles you can set and get statement attributes with SQLSetStmtAttr and SQLGetStmtAttr.

  • SQLHDESC - descriptor handle

    Descriptor handles are rarely used by applications even though they are very useful for more complex operations. Descriptor handles will be covered in later tutorials.

如果要查詢目前 unixODBC 的版本,使用下列的指令:

odbcinst --version

Listing Installed Drivers and Data Sources

如果是使用 unixODBC,可以使用下列命令列出已安裝的資料來源:

odbcinst -q -s

如果自己寫一個程式列出來已安裝的資料來源:

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    char driver[256];
    char attr[256];
    SQLSMALLINT driver_ret;
    SQLSMALLINT attr_ret;
    SQLUSMALLINT direction;
    SQLRETURN ret;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (ret != SQL_SUCCESS) {
        printf("SQLAllocHandle failed.\n");
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);

    direction = SQL_FETCH_FIRST;
    while (SQL_SUCCEEDED(ret = SQLDrivers(env, direction, driver,
                                          sizeof(driver), &driver_ret, attr,
                                          sizeof(attr), &attr_ret))) {
        direction = SQL_FETCH_NEXT;
        printf("%s - %s\n", driver, attr);
        if (ret == SQL_SUCCESS_WITH_INFO)
            printf("\tdata truncation\n");
    }

    if (env != NULL) {
        SQLFreeHandle(SQL_HANDLE_ENV, env);
    }
    return 0;
}

在 Windows 平台,需要連結 odbc32 library,使用 UnixODBC 平台則需要連結 libodbc (-lodbc) 才行。

接下來使用 PostgreSQL ODBC driver 測試,測試是否能夠正確連接到資料庫。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

void extract_error(char *fn, SQLHANDLE handle, SQLSMALLINT type) {
    SQLINTEGER i = 0;
    SQLINTEGER native;
    SQLCHAR state[7];
    SQLCHAR text[256];
    SQLSMALLINT len;
    SQLRETURN ret;

    fprintf(stderr,
            "\n"
            "The driver reported the following diagnostics whilst running "
            "%s\n\n",
            fn);

    do {
        ret = SQLGetDiagRec(type, handle, ++i, state, &native, text,
                            sizeof(text), &len);
        if (SQL_SUCCEEDED(ret))
            printf("%s:%ld:%ld:%s\n", state, i, native, text);
    } while (ret == SQL_SUCCESS);
}

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLRETURN ret;
    SQLCHAR outstr[1024];
    SQLSMALLINT outstrlen;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, outstr,
                           sizeof(outstr), &outstrlen, SQL_DRIVER_COMPLETE);
    if (SQL_SUCCEEDED(ret)) {
        printf("Connected\n");
        printf("Returned connection string was:\n\t%s\n", outstr);
        if (ret == SQL_SUCCESS_WITH_INFO) {
            printf("Driver reported the following diagnostics\n");
            extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
        }
        SQLDisconnect(dbc); /* disconnect from driver */
    } else {
        fprintf(stderr, "Failed to connect\n");
        extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
    }

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

下面列出目前資料庫中表格的資料。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLHSTMT stmt = NULL;
    SQLRETURN ret;
    SQLSMALLINT columns; /* number of columns in result-set */
    SQLCHAR buf[5][64];
    int row = 0;
    SQLLEN indicator[5];
    int i;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0, NULL,
                           SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, "TABLE", SQL_NTS);
    SQLNumResultCols(stmt, &columns);
    for (i = 0; i < columns; i++) {
        SQLBindCol(stmt, i + 1, SQL_C_CHAR, buf[i], sizeof(buf[i]),
                   &indicator[i]);
    }

    /* Fetch the data */
    while (SQL_SUCCEEDED(SQLFetch(stmt))) {
        /* display the results that will now be in the bound area's */
        for (i = 0; i < columns; i++) {
            if (indicator[i] == SQL_NULL_DATA) {
                printf("  Column %u : NULL\n", i);
            } else {
                printf("  Column %u : %s\n", i, buf[i]);
            }
        }
    }

End:
    if (stmt)
        SQLFreeHandle(SQL_HANDLE_DBC, stmt);

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

接下來的程式使用 SQLExecDirect 與 SQLFetch 取得 PostgreSQL 的版本資訊。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    SQLCHAR Version[255];
    SQLLEN siVersion;

    char *sqlStatement = "Select version() as version";

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    ret = SQLExecDirect(hstmt, sqlStatement, strlen(sqlStatement));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecDirect() failed.\n");
        goto End;
    }

    while (1) {
        ret = SQLFetch(hstmt);
        if (ret == SQL_ERROR || ret == SQL_SUCCESS_WITH_INFO) {
            fprintf(stderr, "SQLFetch(hstmt) failed.\n");
            goto End;
        }

        if (ret == SQL_NO_DATA) {
            break;
        }

        if (ret == SQL_SUCCESS) {
            ret = SQLGetData(hstmt, 1, SQL_C_CHAR, Version, 255, &siVersion);
            printf("Versoin:\n%s\n", Version);
        }
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

下面是 SQLPrepare/SQLExecute 的範例(包含使用 SQLBindParameter)。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    int RetParam = 1;
    SQLLEN cbRetParam = SQL_NTS;
    SQLCHAR Name[40];
    SQLLEN lenName = 0;
    SQLSMALLINT NumParams;

    char *sqlstr = NULL;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    sqlstr = "drop table if exists person";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare() failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute(drop) failed.\n");
        goto End;
    }

    sqlstr = "create table if not exists person (id integer, name varchar(40) "
             "not null)";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(create) failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute() failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER,
                           0, 0, &RetParam, 0, &cbRetParam);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(1) failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 40,
                           0, Name, 40, &lenName);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(2) failed.\n");
        goto End;
    }

    ret = SQLPrepare(hstmt,
                     (SQLCHAR *)"insert into person (id, name) values (?, ?)",
                     SQL_NTS);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(insert) failed.\n");
        goto End;
    }

    SQLNumParams(hstmt, &NumParams);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLNumParams() failed.\n");
        goto End;
    }

    printf("Num params : %i\n", NumParams);
    strcpy(Name, "Orange");
    lenName = strlen(Name);

    ret = SQLExecute(hstmt);
    if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) {
        printf("Status : Success\n");
    } else {
        fprintf(stderr, "SQLExecute(insert) failed.\n");
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

C++ Regular Expression

Posix Regular Expression

Posix Regular Expression 是 POSIX 所建立的其中一個標準(並且有 Basic 與 Extended 的分別), 大多數有支援 POSIX 標準的 libc library 都有實作(包含 glibc), 不過實作的細節可能在各個實作上會略有不同。因此,雖然 glibc 有內建,除非要相容於 GNU 工具程式的正規表示式, 否則不一定要使用 Posix Regular Expression。 對於 C 而言,最常被考慮的跨平台 Regular Expression library 為 PCRE 或者是 PCRE2。

下面是 1-9 位數不重複印出來的練習問題(在 Linux 測試,使用 glibc):

#include <stdio.h>
#include <math.h>
#include <regex.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", REG_EXTENDED);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

PCRE 或者是 PCRE2 都有提供 Posix Regular Expression 相容的 API, 只是 Regular Expression 語法就沒有 Basic 與 Extended 的分別, 而是使用 PCRE 本身的語法。以 PCRE2 來說,只要改為 include pcre2posix.h, 連結時要加上 -lpcre2-posix-lpcre2-8 即可。

#include <stdio.h>
#include <math.h>
#include <pcre2posix.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", 0);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

C++ Regular Expression

自 C++11 開始,C++ 標準函式庫提供了 Regular Expression library。

下面是 1-9 位數不重複印出來的練習問題:

#include <cmath>
#include <iostream>
#include <regex>
#include <string>

int main() {
    int number = 0;
    long max = 0;

    std::cout << "Please give a number: ";
    std::cin >> number;

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    std::regex re("([0-9]).*\\1",  std::regex_constants::ECMAScript);
    for (long index = 1; index <= max; index++) {
        std::string s = std::to_string(index);

        std::smatch m;
        std::regex_search(s, m, re);

        if (m.empty()) {
            std::cout << index << std::endl;
        }
    }

    return 0;
}

2024/02/03

GNU Make

GNU Make 是一個工具程式, 經由讀取 Makefile 或者是 makefile 的檔案(也可以使用 -f 指定),自動化建構軟體。 Makefile 是由很多相依性項目(dependencies)和規則(rules)所組成。

GNU Makefile 可以在各個程式語言使用,而不僅限於 C 或者是 C++。
不過因為通常是使用 C 或者是 C++,所以接下來使用 C 作為例子,考慮一個簡單的程式 hello.c:

void say(const char *name);

int main() {
    say("Orange");

    return 0;
}

以及 say procedure 的實作:

#include <stdio.h>

void say(const char *name) { printf("Hello, %s.\n", name); }

那麼在命令列編譯的指令如下:

gcc hello.c say.c -o hello

Makefile 的規則如下:

target [target ...]: [component ...]
	Tab ↹[command 1]
		.
		.
		.
	Tab ↹[command n]

Makefile 支援 Suffix rules,例子如下:

.SUFFIXES: .txt .html

# From .html to .txt
.html.txt:
    lynx -dump $<   >   $@

Makefile 支援 Pattern rules,例子如下:

# From %.html to %.txt
%.txt : %.html
    lynx -dump $< > $@

其中 $@ 或者是 $<, $^, $? 都是 Makefile 的巨集。$@ 代表工作目標的完整檔案名稱,$< 代表觸發建制目標的檔案名稱。 $^ 代表所有的依賴檔案,並以空格隔開這些檔名。$? 代表比目標還要新的依賴檔案列表。而 $* 代表工作目標的主檔名(也就是不包含副檔名)。

撰寫一個 Makefile 如下:

CC = /usr/bin/gcc
CFLAGS= -O2 -Wall
PROGRAM = hello
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))

RM = rm

all: $(PROGRAM)

$(PROGRAM): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) -o $@

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	$(RM) $(PROGRAM) *.o

.PHONY: all clean

2024/01/22

Bye bye, FreeBASIC

我曾經想過為什麼會有人想要寫一個 bye bye 的文章,以前的我沒有搞懂,現在我明白了,因為太過不爽,所以需要寫一篇文章記錄才行。

最近一個月我又(因為之前有試過一次)嘗試看看 FreeBASIC,然後就發現 ODBC 還是一樣只有支援 Windows 平台。我之前就是因為這樣,試到這裡的時候直接放棄,這表示 FreeBASIC 並沒有真正的跨平台,也可以說雖然可以編譯不同平台的原始碼,但是函式庫上的支援很糟糕(小提示:還有一些小地方可以得到這個結論,你可以觀察 crt 下面的 header files,例如 stat 和 select 的部份)。

這一次我比較深入的觀察 FreeBASIC ODBC headers,然後才發現其實只要修改關鍵的型別,加上一些 Windows 上使用的自定型別(例如 WORD),ODBC 程式就可以連結 unixODBC 然後順利編譯並且正確執行。

我發現 FreeBASIC 使用 fbfrog 執行轉換的工作。所以接下來我使用 fbfrog 試著轉換了幾個 FreeBASIC 目前沒有的函式庫(包含 libao, libarcihve, libmicrohttpd, taglib c bindings, and lmdb),雖然有些地方需要手動,不過因為這些函式庫我之前有用過,所以知道怎麼修正問題。

然後我想應該要將結果分享給社群,這樣也可以讓我知道我是不是有地方沒有搞定。我一開始分享在 Community Forum 的訊息是 FreeBASIC ODBC headers (unixODBC version),而文章是需要審核的,所以等待一小段時間以後就看到文章出現了。

接下來我將我轉換的函式庫 header files 資訊貼上去(因為是不同的函式庫,所以集中在一篇文章並不合理,我一個函式庫寫一篇文章),然後等待審核。結果下午我發現我無法登入,我被判定為 spammer,而之前審核通過的文章也直接被刪除。因為文章是有人審核的 ,所以也不要說是程式誤判。那麼如果與 FreeBASIC 相關的文章都無法發表,那是要交流什麼?這個程式語言社群並不歡迎他的使用者。

我很慶幸我在一開始使用就遇到這件事,讓我不會花更多的時間在這個程式語言上,我把我放在 Github 上的成果全部刪除,並且移除了電腦上安裝的 FreeBASIC。我想,就這樣吧,最後說一聲 bye bye。

2023/09/25

橫掃千軍之改朝換代

《橫掃千軍之改朝換代》 (Total Annihilation: Kingdoms) 是由 Cavedog Entertainment 於 1999 年 6 月 25 日開發和發布的即時戰略遊戲, 採用與《橫掃千軍》類似的引擎,還加強了單人戰役的劇情深度。 主要有四個種族(Aramon, Veruna, Zhon 以及 Taros),單人劇情戰役有 48 個關卡。 在發行後因為一些遊戲性的問題而受到不少的批評,不過 patch 2.0 就已經修正了大多數玩家抱怨的問題。

資料片 The Iron Plague 在 2000 年發行,加入了以科技為主的種族 Creon,讓遊戲帶有科幻色彩的蒸汽龐克 (steampunk) 風格, 單人劇情戰役有 25 個關卡,遊戲版本則升級為 3.0。 資料片除了新種族,也增加了其它四個種族的新單位,也包含之前修正的遊戲性問題,因此這是一個成功的資料片。 GOG 目前有出售 Total Annihilation: Kingdoms + Iron Plague, 版本則為 unofficial patch 4.0,經過實測,在 Linux 上使用 Wine 執行情況良好。

橫掃千軍之改朝換代是陸海空三個軍種都有的設計,以及單人戰役模式採用線性的故事結構,類似 RPG 玩法的一些關卡十分有趣, 並且加上我喜歡這個遊戲的音樂,因此雖然有一些小缺點,仍然是我喜愛的遊戲之一。

《橫掃千軍之改朝換代》也和《橫掃千軍》一樣,實作了士兵升級系統,當戰鬥單位殺了一定的人數或者是毀掉一定的建築以後, 就會升級成為老兵,造型也會發生變化(顏色的不同代表代表護甲的升級)。雖然即時戰略遊戲的士兵更換率很快,仍然是一個有趣的設計。

如果想省略一開始的動畫,使用 -skiplogo 參數(不適用於unofficial patch 4.1)。
(patch 4.1 是為了網路對戰的 unofficial patch,我只有玩單人戰役,所以停留在 4.0)

可以參考的資料:


Faction Monarch Attack Ability
Aramon (Earth) Elsin (The Mage King) lightning, meteor, and earthquake raise the dead
Taros (Fire) Lokken (The Necromancer) fireball, tracking fireball and fire wave cloak (hotkey K)
Veruna (Water) Kirenna (The Sea Mage) water burst, water ball, and water wave can swim
Zhon (Wind) Thirsha (The Huntress) lightning, ball lightning, and wind wave can fly
Creon The Sage Blue flame, mortar, energy blast

《橫掃千軍之改朝換代》在對電腦的單人戰役都是以某個勢力的君主 (Monarch) 作為起點(在單人劇情戰役有些關卡不是這樣)。 在對電腦的單人戰役,可以選擇君主死亡以後遊戲是否要繼續。預設值為否,在這個設定下如果君主死亡就表示輸了。
另外,我會允許 Map Revealed,這樣可以在一開始的時候就知道全部地圖的基本情況。

五個種族都有飛行偵查單位,其中包含了 'Spyhawk' for Aramon, 'Parrot' for Veruna, 'Gargoyle' for Taros, 'Bat' for Zhon, 'Barnstormer' for Creon。

所有的種族都有 Dragon unit,其中包含了 'Golden Dragon' for Aramon, 'Sea Dragon' for Veruna, 'Black Dragon' for Taros, 'Ancient Dragon' for Zhon, 'Aerial Juggernaut' for Creon,在遊戲中限制為同一時間內只能只有一隻。

Aramon 的 Acolyte of Anu, Veruna 的 Priest of Lihr, Taros 的 Dark Priest, Zhon 的 Shaman, Creon 的 Chief Engineer 是 Dragon unit 的建造者, 《橫掃千軍之改朝換代》 其中一個有趣的設計就是如果想要加快建造的速度,可以使用其它的建造者幫忙快速建造。

所有的種族都有 God unit,其中包含了 'Anu: Deity of Aramon' for Aramon, 'Lihr: Deity of Veruna' for Veruna, 'Beliel - The Deity of Taros' for Taros, 'Tammuz: Deity of Zho' for Zhon, 'Ghost of Garacaius' for Creon, 在遊戲中限制為同一時間內只能只有一隻。因為建造時間與資源的關係,很難看到 God unit 出現在戰場上。


Hotkeys:
C = Clear
K = Cloak on/off
L = Load unit into transport
T = Track unit
U = Unload unit from transport
F1 = Game options
F4 = Leaderboard
F9 = Screen shot
TAB = Full screen radar
CTRL + A = Select all units
CTRL + D = Dismiss selected units
CTRL + M = Select and track monarch
CTRL + Z = Select all like units
CTRL + 1~9 = Create squad
CTRL F5–F8 = Set bookmark location
F5–F8 = Return to bookmarked location
《橫掃千軍之改朝換代》 沒有支援雙擊滑鼠左鍵選擇螢幕中同類型單位的功能,但是可以使用快捷鍵 CTRL + Z 來選擇全部同類型單位。

如果習慣點擊空地攻擊以後,單位會邊移動邊戰鬥,《橫掃千軍之改朝換代》 有支援但是沒有加入預設的按鍵, 這可以修改 Keys.TDF達到目的

    LOWER_A = UnitCommand Attack;
    LOWER_B =;
    LOWER_C = UnitCommand Clear;
    LOWER_D = DiplomacyMenu;
    LOWER_E =;
    LOWER_F = UnitCommand MoveFight;
    LOWER_G = UnitCommand Guard;
    UPPER_A = UnitCommand Attack;
    UPPER_B =;
    UPPER_C = UnitCommand Clear;
    UPPER_D = DiplomacyMenu;
    UPPER_E =;
    UPPER_F = UnitCommand MoveFight;
    UPPER_G = UnitCommand Guard;

在這個設定下,就會指定 F 鍵是邊移動邊戰鬥的按鍵。


《橫掃千軍之改朝換代》為了降低復雜度,所以資源只有一種:Mana,並且使用了收集器的做法, 也就是玩家不用製造工人去採集資源,而是建立收集器(Lodestone 與進階的 Divine Lodestone)採集魔力資源。 因此,重點在佔領、搶奪與保護 Mana site,並且將生產的士兵送到合適的地方。

大體而言(Zhon 的生產體制比較特殊,不過一樣的原則),在遊戲一開始的時候先使用君主建立一級建築(Aramon Barracks, Taros Cabal. Veruna Enclave, Creon Smithy,與 Zhon 可以用來類比的 Beast Handler),開始使用 Mana 資源。 建好後先生產一個 builder(Aramon Mage Builder, Taros Dark Mason. Veruna Priestess, Creon Mechanic,與 Zhon 可以用來類比的 Beast Handler), 然後開始生產基本兵力。

Zhon 的移動力是五個種族中最好的,其生產方式也是比較特別的種族,產兵並不是從建築產生,而是直接生產各種單位。 如果要連續生產,一個是使用 SHIFT 鍵安排要生產的單位, 一個是按住 CTRL 鍵再選擇要生產的單位(出現 +++ 就表示成功)。
Thirsha 是五位君主中移動速度最快的,也是惟一無法製造 walls 和 gate 的君主。
Troll 是初期裡強健的 melee 單位,配合 Hunter 在初期是不錯的組合。 資料片新增加 Swamp beast,是可以二棲作戰的強壯戰士,不過在陸地的移動速度較為緩慢。
Spirit Wolf 也是資料片新增加的單位,是移動快速的 melee 單位。
Drakes 是強健的空軍單位,Harpies 則具有一定機率將對方單位轉為自己友軍的能力。
Zhon 的運輸單位是會飛行的 Roc,Roc 本身沒有攻擊能力,而且無法運輸 Stone Giant(設定為因為 Stone Giant 重量的關係所以無法運送)。
Zhon 的防禦建築只有 Death Totem 一種防禦建築(雖然如此,配合 Stone Giant 仍然可以建立不錯的防禦)。

Veruna 的君主 Kirenna 可以建造 Enclave 與 Sea Fort (用來建造船隻)。
Veruna 擁有遊戲中最強的海軍,Man of War 是整個遊戲中最強健的海軍單位(也可作為運輸船使用),因為無法對空, 所以通常需要與 Harpoon Ship 搭配。Flagship 可以用來建造 enclave, lodestone, Sea Fort 與 Pontoon Tower。 Skiff 在資料片之後被加入了 Heal 能力。Trebuchet Ship 是有遠距射程的船隻,但是無法運輸其它單位,造價較貴且移動緩慢,通常用來對抗地面炮塔使用。
Veruna 的 Mortar 在攻擊距離上不如 Aramon 的 Trebuchet,但也是強大的長程地面炮塔。Mer warrior 是資料片新增加的單位,是可以二棲作戰的戰士。
除了偵查用的 Parrot 與 Sea Dragon,Veruna 在空軍的兵種就只有 Dirigible 這個飛行單位。
Veruna 的防禦建築為 Guard Tower, Bastion, Mortar, Lighthouse (資料片新增,與其它的防禦建築形成良好的互補) 與 Floating Tower (海上使用)。

Taros 的君主 Lokken 是第二強大的君主,具有隱形的能力。
Taros 有些特別的地方在於 Lokken 在一開始就可以建造 Cabal, Abyss 與 Temple 這三種建築。 也就是如果玩家需要,可以在略早期的時候就建造建築並且生產特殊的兵種。 舉例來說,Blade demon 是很強壯的 melee 單位;Sky Knight 是強健的空軍單位;Weather witches 為擁有廣域攻擊法術的單位, Mind Mage 具有將對方的一個單位或者是多個單位轉為自己友軍的能力(但是需要施法時間與使用技巧) 。
Executioner 是初期裡強健的 melee 單位。Ghost Ship 的攻擊力普通,但是具有跨越地域與海洋的能力,並且可以作為運輸單位使用。
Taros 缺少長距離的炮兵單位,不過長距離的炮兵單位可以使用 Fire Demon 作為代替,因為 Fire Demon 也具有長距離的攻擊範圍。或者也可以使用資料片新增加的 Rictus 作為輔助。
還要注意的是,Taros 並沒有海軍單位!
不過有趣的地方在 Taros 有一個單位是會游泳的,那就是 Lich;Lich 是需要小心使用的單位,他的攻擊是敵友軍都會受到傷害的廣域攻擊。
Taros 的防禦建築為 Caged Demon, Mage Tower。Taros 的炮塔相對其它的的種族來說並不強,在炮塔這點來說是較為弱勢的。

Aramon 擁有最強的君主 Elsin,Elsin 的 raise the dead 能力也可以復活敵人的單位。
Aramon 是一個陸地強權的種族,包含了 Knight 與 Mage archer 等強健的戰鬥單位, 並且擁有射程最遠的地面炮塔 Trebuchet。
但是能夠生產船隻只有二種,Ark(資料片新增的單位)由 Elsin 直接製造, War Galley 是由 Mage Builder 與 Flying Pegasus 直接製造,如果需要連續造船的時候,也可以使用 SHIFT 或者是 CTRL 的方式。
Aramon 在空軍方面比較弱勢,Flying Pegasus 是資料片新增加的單位,和 Mage Builder 一樣是 bulder, 但是具有飛行的優勢,可以跨越地域建立建築。
資料片新增加的 Rolling tower 是移動式的防禦單位,移動速度緩慢,擁有不錯的對空能力。
Aramon 的防禦建築為 Watch Tower, Stronghold 與 Trebuchet。

Creon 與 Veruna 在君主的建築上有些類似的地方,The Sage 可以建造 Smithy 與 Navy Yard (用來建造船隻)。
Creon 的單位相較其它四個種族比較少,Automaton 的移動速度不快,Creon 在初期缺少強壯或者是移動速度快的 melee 單位是一個弱點。
在度過初期以後,在 Academy 生產的 Beast Rider (可以切換遠程與近身攻擊) 與 Neo-Dragon (三種攻擊方式) 都是很好用的單位。 Shock Trooper 則需要小心使用,因為他有可能會誤傷友軍。
Creon 的防禦建築為 Gatling Crossbow, Bomb Sprinkler 與 Prismatic Mirror。

Wine

Wine 是在 x86、x86-64 容許 Unix-like 作業系統在 X Window System 運行 Microsoft Windows 程式的軟體。

下面是在 openSUSE Tumbleweed 安裝的指令:

sudo zypper in wine wine-mono

(不先安裝 wine-mono 也可以,Wine 在一開始的時候如果偵測發現沒有安裝,會安裝一份)

然後使用下列的指令建立一個 32 位元的模擬器:

WINEARCH=win32 wine wineboot

如果要特別為一個遊戲建立 WINE 目錄,使用下列的方式:

WINEPREFIX=~/.winems WINEARCH="win32" wine wineboot

有些遊戲需要 OpenAL,如果沒有 wine-openal 套件可以安裝,也可以透過 OpenAL Windows Installer 安裝。

如果需要 MP3 等 codecs(有些遊戲會需要),使用下列的指令安裝 gstreamer 的 plugin:

sudo zypper in gstreamer-plugins-base-32bit gstreamer-plugins-good-32bit \
gstreamer-plugins-bad-32bit gstreamer-plugins-ugly-32bit gstreamer-plugins-libav-32bit

注意:如果 OpenGL 會初始化失敗,檢查是否有安裝 Mesa 32bit 版本的軟體:

sudo zypper in Mesa-libGL1-32bit

使用 winecfg 進行設定。也可以使用 wine control 來設定(類似 Windows 的控制台)。

使用 winetricks 安裝可能需要的程式。

如果要使用 command prompt:

wine cmd

如果要移除安裝的程式:

wine uninstaller

有些遊戲結束以後並沒有正確的設回螢幕解析度,在 X Window 環境下可以這樣設定(以 1366x768 為例):

xrandr -s 1366x768