code:
import { RequestHandler, Request, Router } from "express";
import * as http from "http";
import { createProxyMiddleware } from "http-proxy-middleware";
import { config } from "../config";
import { keyPool } from "../key-management";
import { logger } from "../logger";
import { createQueueMiddleware } from "./queue";
import { ipLimiter } from "./rate-limit";
import { handleProxyError } from "./middleware/common";
import {
  addKey,
  blockZoomerOrigins,
  createPreprocessorMiddleware,
  finalizeBody,
  languageFilter,
  limitCompletions,
  removeOriginHeaders,
} from "./middleware/request";
import {
  createOnProxyResHandler,
  ProxyResHandlerWithBody,
} from "./middleware/response";

let modelsCache: any = null;
let modelsCacheTime = 0;

function getModelsResponse() {
  if (new Date().getTime() - modelsCacheTime < 1000 * 60) {
    return modelsCache;
  }

  // https://platform.openai.com/docs/models/overview
  const gptVariants = [
    "gpt-4",
    "gpt-4-0613",
    "gpt-4-0314", // EOL 2023-09-13
    "gpt-4-32k",
    "gpt-4-32k-0613",
    "gpt-4-32k-0314", // EOL 2023-09-13
    "gpt-3.5-turbo",
    "gpt-3.5-turbo-0301", // EOL 2023-09-13
    "gpt-3.5-turbo-0613",
    "gpt-3.5-turbo-16k",
    "gpt-3.5-turbo-16k-0613",
  ];

  const gpt4Available = keyPool.list().filter((key) => {
    return key.service === "openai" && !key.isDisabled && key.isGpt4;
  }).length;

  const models = gptVariants
    .map((id) => ({
      id,
      object: "model",
      created: new Date().getTime(),
      owned_by: "openai",
      permission: [
        {
          id: "modelperm-" + id,
          object: "model_permission",
          created: new Date().getTime(),
          organization: "*",
          group: null,
          is_blocking: false,
        },
      ],
      root: id,
      parent: null,
    }))
    .filter((model) => {
      if (model.id.startsWith("gpt-4")) {
        return gpt4Available > 0;
      }
      return true;
    });

  modelsCache = { object: "list", data: models };
  modelsCacheTime = new Date().getTime();

  return modelsCache;
}

const handleModelRequest: RequestHandler = (_req, res) => {
  res.status(200).json(getModelsResponse());
};

const rewriteRequest = (
  proxyReq: http.ClientRequest,
  req: Request,
  res: http.ServerResponse
) => {
  const rewriterPipeline = [
    addKey,
    languageFilter,
    limitCompletions,
    blockZoomerOrigins,
    removeOriginHeaders,
    finalizeBody,
  ];

  try {
    for (const rewriter of rewriterPipeline) {
      rewriter(proxyReq, req, res, {});
    }
  } catch (error) {
    req.log.error(error, "Error while executing proxy rewriter");
    proxyReq.destroy(error as Error);
  }
};

const openaiResponseHandler: ProxyResHandlerWithBody = async (
  _proxyRes,
  req,
  res,
  body
) => {
  if (typeof body !== "object") {
    throw new Error("Expected body to be an object");
  }

  if (config.promptLogging) {
    const host = req.get("host");
    body.proxy_note = `Prompts are logged on this proxy instance. See ${host} for more information.`;
  }

  // TODO: Remove once tokenization is stable
  if (req.debug) {
    body.proxy_tokenizer_debug_info = req.debug;
  }

  res.status(200).json(body);
};

const openaiProxy = createQueueMiddleware(
  createProxyMiddleware({
    target: "https://api.openai.com",
    changeOrigin: true,
    on: {
      proxyReq: rewriteRequest,
      proxyRes: createOnProxyResHandler([openaiResponseHandler]),
      error: handleProxyError,
    },
    selfHandleResponse: true,
    logger,
  })
);

const openaiRouter = Router();
// Fix paths because clients don't consistently use the /v1 prefix.
openaiRouter.use((req, _res, next) => {
  if (!req.path.startsWith("/v1/")) {
    req.url = `/v1${req.url}`;
  }
  next();
});
openaiRouter.get("/v1/models", handleModelRequest);
openaiRouter.post(
  "/v1/chat/completions",
  ipLimiter,
  createPreprocessorMiddleware({ inApi: "openai", outApi: "openai" }),
  openaiProxy
);
// Redirect browser requests to the homepage.
openaiRouter.get("*", (req, res, next) => {
  const isBrowser = req.headers["user-agent"]?.includes("Mozilla");
  if (isBrowser) {
    res.redirect("/");
  } else {
    next();
  }
});
openaiRouter.use((req, res) => {
  req.log.warn(`Blocked openai proxy request: ${req.method} ${req.path}`);
  res.status(404).json({ error: "Not found" });
});

export const openai = openaiRouter;


code:
/**
 * Very scuffed request queue. OpenAI's GPT-4 keys have a very strict rate limit
 * of 40000 generated tokens per minute. We don't actually know how many tokens
 * a given key has generated, so our queue will simply retry requests that fail
 * with a non-billing related 429 over and over again until they succeed.
 *
 * Dequeueing can operate in one of two modes:
 * - 'fair': requests are dequeued in the order they were enqueued.
 * - 'random': requests are dequeued randomly, not really a queue at all.
 *
 * When a request to a proxied endpoint is received, we create a closure around
 * the call to http-proxy-middleware and attach it to the request. This allows
 * us to pause the request until we have a key available. Further, if the
 * proxied request encounters a retryable error, we can simply put the request
 * back in the queue and it will be retried later using the same closure.
 */

import type { Handler, Request } from "express";
import { keyPool, SupportedModel } from "../key-management";
import { logger } from "../logger";
import { AGNAI_DOT_CHAT_IP } from "./rate-limit";
import { buildFakeSseMessage } from "./middleware/common";

export type QueuePartition = "claude" | "turbo" | "gpt-4";

const queue: Request[] = [];
const log = logger.child({ module: "request-queue" });

/** Maximum number of queue slots for Agnai.chat requests. */
const AGNAI_CONCURRENCY_LIMIT = 15;
/** Maximum number of queue slots for individual users. */
const USER_CONCURRENCY_LIMIT = 1;

/**
 * Returns a unique identifier for a request. This is used to determine if a
 * request is already in the queue.
 * This can be (in order of preference):
 * - user token assigned by the proxy operator
 * - x-risu-tk header, if the request is from RisuAI.xyz
 * - IP address
 */
function getIdentifier(req: Request) {
  if (req.user) {
    return req.user.token;
  }
  if (req.risuToken) {
    return req.risuToken;
  }
  return req.ip;
}

const sameUserPredicate = (incoming: Request) => (queued: Request) => {
  const queuedId = getIdentifier(queued);
  const incomingId = getIdentifier(incoming);
  return queuedId === incomingId;
};

export function enqueue(req: Request) {
  const enqueuedRequestCount = queue.filter(sameUserPredicate(req)).length;
  let isGuest = req.user?.token === undefined;

  // All Agnai.chat requests come from the same IP, so we allow them to have
  // more spots in the queue. Can't make it unlimited because people will
  // intentionally abuse it.
  // Authenticated users always get a single spot in the queue.
  const maxConcurrentQueuedRequests =
    isGuest && req.ip === AGNAI_DOT_CHAT_IP
      ? AGNAI_CONCURRENCY_LIMIT
      : USER_CONCURRENCY_LIMIT;
  if (enqueuedRequestCount >= maxConcurrentQueuedRequests) {
    if (req.ip === AGNAI_DOT_CHAT_IP) {
      // Re-enqueued requests are not counted towards the limit since they
      // already made it through the queue once.
      if (req.retryCount === 0) {
        throw new Error("Too many agnai.chat requests are already queued");
      }
    } else {
      throw new Error("Your IP or token already has a request in the queue");
    }
  }

  queue.push(req);
  req.queueOutTime = 0;

  // shitty hack to remove hpm's event listeners on retried requests
  removeProxyMiddlewareEventListeners(req);

  // If the request opted into streaming, we need to register a heartbeat
  // handler to keep the connection alive while it waits in the queue. We
  // deregister the handler when the request is dequeued.
  if (req.body.stream === "true" || req.body.stream === true) {
    const res = req.res!;
    if (!res.headersSent) {
      initStreaming(req);
    }
    req.heartbeatInterval = setInterval(() => {
      if (process.env.NODE_ENV === "production") {
        req.res!.write(": queue heartbeat\n\n");
      } else {
        req.log.info(`Sending heartbeat to request in queue.`);
        const partition = getPartitionForRequest(req);
        const avgWait = Math.round(getEstimatedWaitTime(partition) / 1000);
        const currentDuration = Math.round((Date.now() - req.startTime) / 1000);
        const debugMsg = `queue length: ${queue.length}; elapsed time: ${currentDuration}s; avg wait: ${avgWait}s`;
        req.res!.write(buildFakeSseMessage("heartbeat", debugMsg, req));
      }
    }, 10000);
  }

  // Register a handler to remove the request from the queue if the connection
  // is aborted or closed before it is dequeued.
  const removeFromQueue = () => {
    req.log.info(`Removing aborted request from queue.`);
    const index = queue.indexOf(req);
    if (index !== -1) {
      queue.splice(index, 1);
    }
    if (req.heartbeatInterval) {
      clearInterval(req.heartbeatInterval);
    }
  };
  req.onAborted = removeFromQueue;
  req.res!.once("close", removeFromQueue);

  if (req.retryCount ?? 0 > 0) {
    req.log.info({ retries: req.retryCount }, `Enqueued request for retry.`);
  } else {
    req.log.info(`Enqueued new request.`);
  }
}

function getPartitionForRequest(req: Request): QueuePartition {
  // There is a single request queue, but it is partitioned by model and API
  // provider.
  // - claude: requests for the Anthropic API, regardless of model
  // - gpt-4: requests for the OpenAI API, specifically for GPT-4 models
  // - turbo: effectively, all other requests
  const provider = req.outboundApi;
  const model = (req.body.model as SupportedModel) ?? "gpt-3.5-turbo";
  if (provider === "anthropic") {
    return "claude";
  }
  if (provider === "openai" && model.startsWith("gpt-4")) {
    return "gpt-4";
  }
  return "turbo";
}

function getQueueForPartition(partition: QueuePartition): Request[] {
  return queue.filter((req) => getPartitionForRequest(req) === partition);
}

export function dequeue(partition: QueuePartition): Request | undefined {
  const modelQueue = getQueueForPartition(partition);

  if (modelQueue.length === 0) {
    return undefined;
  }

  const req = modelQueue.reduce((prev, curr) =>
    prev.startTime < curr.startTime ? prev : curr
  );
  queue.splice(queue.indexOf(req), 1);

  if (req.onAborted) {
    req.res!.off("close", req.onAborted);
    req.onAborted = undefined;
  }

  if (req.heartbeatInterval) {
    clearInterval(req.heartbeatInterval);
  }

  // Track the time leaving the queue now, but don't add it to the wait times
  // yet because we don't know if the request will succeed or fail. We track
  // the time now and not after the request succeeds because we don't want to
  // include the model processing time.
  req.queueOutTime = Date.now();
  return req;
}

/**
 * Naive way to keep the queue moving by continuously dequeuing requests. Not
 * ideal because it limits throughput but we probably won't have enough traffic
 * or keys for this to be a problem.  If it does we can dequeue multiple
 * per tick.
 **/
function processQueue() {
  // This isn't completely correct, because a key can service multiple models.
  // Currently if a key is locked out on one model it will also stop servicing
  // the others, because we only track one rate limit per key.
  const gpt4Lockout = keyPool.getLockoutPeriod("gpt-4");
  const turboLockout = keyPool.getLockoutPeriod("gpt-3.5-turbo");
  const claudeLockout = keyPool.getLockoutPeriod("claude-v1");

  const reqs: (Request | undefined)[] = [];
  if (gpt4Lockout === 0) {
    reqs.push(dequeue("gpt-4"));
  }
  if (turboLockout === 0) {
    reqs.push(dequeue("turbo"));
  }
  if (claudeLockout === 0) {
    reqs.push(dequeue("claude"));
  }

  reqs.filter(Boolean).forEach((req) => {
    if (req?.proceed) {
      req.log.info({ retries: req.retryCount }, `Dequeuing request.`);
      req.proceed();
    }
  });
  setTimeout(processQueue, 50);
}

/**
 * Kill stalled requests after 5 minutes, and remove tracked wait times after 2
 * minutes.
 **/
function cleanQueue() {
  const now = Date.now();
  const oldRequests = queue.filter(
    (req) => now - (req.startTime ?? now) > 5 * 60 * 1000
  );
  oldRequests.forEach((req) => {
    req.log.info(`Removing request from queue after 5 minutes.`);
    killQueuedRequest(req);
  });

  const index = waitTimes.findIndex(
    (waitTime) => now - waitTime.end > 300 * 1000
  );
  const removed = waitTimes.splice(0, index + 1);
  log.trace(
    { stalledRequests: oldRequests.length, prunedWaitTimes: removed.length },
    `Cleaning up request queue.`
  );
  setTimeout(cleanQueue, 20 * 1000);
}

export function start() {
  processQueue();
  cleanQueue();
  log.info(`Started request queue.`);
}

let waitTimes: { partition: QueuePartition; start: number; end: number }[] = [];

/** Adds a successful request to the list of wait times. */
export function trackWaitTime(req: Request) {
  waitTimes.push({
    partition: getPartitionForRequest(req),
    start: req.startTime!,
    end: req.queueOutTime ?? Date.now(),
  });
}

/** Returns average wait time in milliseconds. */
export function getEstimatedWaitTime(partition: QueuePartition) {
  const now = Date.now();
  const recentWaits = waitTimes.filter(
    (wt) => wt.partition === partition && now - wt.end < 300 * 1000
  );
  if (recentWaits.length === 0) {
    return 0;
  }

  return (
    recentWaits.reduce((sum, wt) => sum + wt.end - wt.start, 0) /
    recentWaits.length
  );
}

export function getQueueLength(partition: QueuePartition | "all" = "all") {
  if (partition === "all") {
    return queue.length;
  }
  const modelQueue = getQueueForPartition(partition);
  return modelQueue.length;
}

export function createQueueMiddleware(proxyMiddleware: Handler): Handler {
  return (req, res, next) => {
    req.proceed = () => {
      proxyMiddleware(req, res, next);
    };

    try {
      enqueue(req);
    } catch (err: any) {
      req.res!.status(429).json({
        type: "proxy_error",
        message: err.message,
        stack: err.stack,
        proxy_note: `Only one request can be queued at a time. If you don't have another request queued, your IP or user token might be in use by another request.`,
      });
    }
  };
}

function killQueuedRequest(req: Request) {
  if (!req.res || req.res.writableEnded) {
    req.log.warn(`Attempted to terminate request that has already ended.`);
    return;
  }
  const res = req.res;
  try {
    const message = `Your request has been terminated by the proxy because it has been in the queue for more than 5 minutes. The queue is currently ${queue.length} requests long.`;
    if (res.headersSent) {
      const fakeErrorEvent = buildFakeSseMessage(
        "proxy queue error",
        message,
        req
      );
      res.write(fakeErrorEvent);
      res.end();
    } else {
      res.status(500).json({ error: message });
    }
  } catch (e) {
    req.log.error(e, `Error killing stalled request.`);
  }
}

function initStreaming(req: Request) {
  req.log.info(`Initiating streaming for new queued request.`);
  const res = req.res!;
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("X-Accel-Buffering", "no"); // nginx-specific fix
  res.flushHeaders();
  res.write("\n");
  res.write(": joining queue\n\n");
}

/**
 * http-proxy-middleware attaches a bunch of event listeners to the req and
 * res objects which causes problems with our approach to re-enqueuing failed
 * proxied requests. This function removes those event listeners.
 * We don't have references to the original event listeners, so we have to
 * look through the list and remove HPM's listeners by looking for particular
 * strings in the listener functions. This is an astoundingly shitty way to do
 * this, but it's the best I can come up with.
 */
function removeProxyMiddlewareEventListeners(req: Request) {
  // node_modules/http-proxy-middleware/dist/plugins/default/debug-proxy-errors-plugin.js:29
  // res.listeners('close')
  const RES_ONCLOSE = `Destroying proxyRes in proxyRes close event`;
  // node_modules/http-proxy-middleware/dist/plugins/default/debug-proxy-errors-plugin.js:19
  // res.listeners('error')
  const RES_ONERROR = `Socket error in proxyReq event`;
  // node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js:146
  // req.listeners('aborted')
  const REQ_ONABORTED = `proxyReq.abort()`;
  // node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js:156
  // req.listeners('error')
  const REQ_ONERROR = `if (req.socket.destroyed`;

  const res = req.res!;

  const resOnClose = res
    .listeners("close")
    .find((listener) => listener.toString().includes(RES_ONCLOSE));
  if (resOnClose) {
    res.removeListener("close", resOnClose as any);
  }

  const resOnError = res
    .listeners("error")
    .find((listener) => listener.toString().includes(RES_ONERROR));
  if (resOnError) {
    res.removeListener("error", resOnError as any);
  }

  const reqOnAborted = req
    .listeners("aborted")
    .find((listener) => listener.toString().includes(REQ_ONABORTED));
  if (reqOnAborted) {
    req.removeListener("aborted", reqOnAborted as any);
  }

  const reqOnError = req
    .listeners("error")
    .find((listener) => listener.toString().includes(REQ_ONERROR));
  if (reqOnError) {
    req.removeListener("error", reqOnError as any);
  }
}

repo_name: subwolfy/subwolfsthing

license: unknown


code:
import { Request, Response, NextFunction } from "express";
import { config } from "../config";

export const AGNAI_DOT_CHAT_IP = "157.230.249.32";

const RATE_LIMIT_ENABLED = Boolean(config.modelRateLimit);
const RATE_LIMIT = Math.max(1, config.modelRateLimit);
const ONE_MINUTE_MS = 60 * 1000;

const lastAttempts = new Map<string, number[]>();

const expireOldAttempts = (now: number) => (attempt: number) =>
  attempt > now - ONE_MINUTE_MS;

const getTryAgainInMs = (ip: string) => {
  const now = Date.now();
  const attempts = lastAttempts.get(ip) || [];
  const validAttempts = attempts.filter(expireOldAttempts(now));

  if (validAttempts.length >= RATE_LIMIT) {
    return validAttempts[0] - now + ONE_MINUTE_MS;
  } else {
    lastAttempts.set(ip, [...validAttempts, now]);
    return 0;
  }
};

const getStatus = (ip: string) => {
  const now = Date.now();
  const attempts = lastAttempts.get(ip) || [];
  const validAttempts = attempts.filter(expireOldAttempts(now));
  return {
    remaining: Math.max(0, RATE_LIMIT - validAttempts.length),
    reset: validAttempts.length > 0 ? validAttempts[0] + ONE_MINUTE_MS : now,
  };
};

/** Prunes attempts and IPs that are no longer relevant after one minutes. */
const clearOldAttempts = () => {
  const now = Date.now();
  for (const [ip, attempts] of lastAttempts.entries()) {
    const validAttempts = attempts.filter(expireOldAttempts(now));
    if (validAttempts.length === 0) {
      lastAttempts.delete(ip);
    } else {
      lastAttempts.set(ip, validAttempts);
    }
  }
};
setInterval(clearOldAttempts, 10 * 1000);

export const getUniqueIps = () => {
  return lastAttempts.size;
};

export const ipLimiter = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (!RATE_LIMIT_ENABLED) {
    next();
    return;
  }

  // Exempt Agnai.chat from rate limiting since it's shared between a lot of
  // users. Dunno how to prevent this from being abused without some sort of
  // identifier sent from Agnaistic to identify specific users.
  if (req.ip === AGNAI_DOT_CHAT_IP) {
    next();
    return;
  }

  // If user is authenticated, key rate limiting by their token. Otherwise, key
  // rate limiting by their IP address. Mitigates key sharing.
  const rateLimitKey = req.user?.token || req.risuToken || req.ip;

  const { remaining, reset } = getStatus(rateLimitKey);
  res.set("X-RateLimit-Limit", config.modelRateLimit.toString());
  res.set("X-RateLimit-Remaining", remaining.toString());
  res.set("X-RateLimit-Reset", reset.toString());

  const tryAgainInMs = getTryAgainInMs(rateLimitKey);
  if (tryAgainInMs > 0) {
    res.set("Retry-After", tryAgainInMs.toString());
    res.status(429).json({
      error: {
        type: "proxy_rate_limited",
        message: `This proxy is rate limited to ${
          config.modelRateLimit
        } prompts per minute. Please try again in ${Math.ceil(
          tryAgainInMs / 1000
        )} seconds.`,
      },
    });
  } else {
    next();
  }
};


code:
/* Accepts incoming requests at either the /kobold or /openai routes and then
routes them to the appropriate handler to be forwarded to the OpenAI API.
Incoming OpenAI requests are more or less 1:1 with the OpenAI API, but only a
subset of the API is supported. Kobold requests must be transformed into
equivalent OpenAI requests. */

import * as express from "express";
import { gatekeeper } from "./auth/gatekeeper";
import { checkRisuToken } from "./auth/check-risu-token";
import { kobold } from "./kobold";
import { openai } from "./openai";
import { anthropic } from "./anthropic";

const proxyRouter = express.Router();
proxyRouter.use(
  express.json({ limit: "1536kb" }),
  express.urlencoded({ extended: true, limit: "1536kb" })
);
proxyRouter.use(gatekeeper);
proxyRouter.use(checkRisuToken);
proxyRouter.use((req, _res, next) => {
  req.startTime = Date.now();
  req.retryCount = 0;
  next();
});
proxyRouter.use("/kobold", kobold);
proxyRouter.use("/openai", openai);
proxyRouter.use("/anthropic", anthropic);
export { proxyRouter as proxyRouter };


code:
import { assertConfigIsValid, config } from "./config";
import "source-map-support/register";
import express from "express";
import cors from "cors";
import path from "path";
import pinoHttp from "pino-http";
import childProcess from "child_process";
import { logger } from "./logger";
import { keyPool } from "./key-management";
import { adminRouter } from "./admin/routes";
import { proxyRouter } from "./proxy/routes";
import { handleInfoPage } from "./info-page";
import { logQueue } from "./prompt-logging";
import { start as startRequestQueue } from "./proxy/queue";
import { init as initUserStore } from "./proxy/auth/user-store";
import { init as initTokenizers } from "./tokenization";
import { checkOrigin } from "./proxy/check-origin";

const PORT = config.port;

const app = express();
// middleware
app.use(
  pinoHttp({
    quietReqLogger: true,
    logger,
    autoLogging: {
      ignore: (req) => {
        const ignored = ["/proxy/kobold/api/v1/model", "/health"];
        return ignored.includes(req.url as string);
      },
    },
    redact: {
      paths: [
        "req.headers.cookie",
        'res.headers["set-cookie"]',
        "req.headers.authorization",
        'req.headers["x-api-key"]',
        // Don't log the prompt text on transform errors
        "body.messages",
        "body.prompt",
      ],
      censor: "********",
    },
  })
);

// TODO: Detect (or support manual configuration of) whether the app is behind
// a load balancer/reverse proxy, which is necessary to determine request IP
// addresses correctly.
app.set("trust proxy", true);

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.get("/health", (_req, res) => res.sendStatus(200));
app.use(cors());
app.use(checkOrigin);

// routes
app.get("/", handleInfoPage);
app.use("/admin", adminRouter);
app.use("/proxy", proxyRouter);

// 500 and 404
app.use((err: any, _req: unknown, res: express.Response, _next: unknown) => {
  if (err.status) {
    res.status(err.status).json({ error: err.message });
  } else {
    logger.error(err);
    res.status(500).json({
      error: {
        type: "proxy_error",
        message: err.message,
        stack: err.stack,
        proxy_note: `Reverse proxy encountered an internal server error.`,
      },
    });
  }
});
app.use((_req: unknown, res: express.Response) => {
  res.status(404).json({ error: "Not found" });
});

async function start() {
  logger.info("Server starting up...");
  await setBuildInfo();

  logger.info("Checking configs and external dependencies...");
  await assertConfigIsValid();

  keyPool.init();

  await initTokenizers();

  if (config.gatekeeper === "user_token") {
    await initUserStore();
  }

  if (config.promptLogging) {
    logger.info("Starting prompt logging...");
    logQueue.start();
  }

  logger.info("Starting request queue...");
  startRequestQueue();

  app.listen(PORT, async () => {
    logger.info({ port: PORT }, "Now listening for connections.");
    registerUncaughtExceptionHandler();
  });

  logger.info(
    { build: process.env.BUILD_INFO, nodeEnv: process.env.NODE_ENV },
    "Startup complete."
  );
}

function registerUncaughtExceptionHandler() {
  process.on("uncaughtException", (err: any) => {
    logger.error(
      { err, stack: err?.stack },
      "UNCAUGHT EXCEPTION. Please report this error trace."
    );
  });
  process.on("unhandledRejection", (err: any) => {
    logger.error(
      { err, stack: err?.stack },
      "UNCAUGHT PROMISE REJECTION. Please report this error trace."
    );
  });
}

/**
 * Attepts to collect information about the current build from either the
 * environment or the git repo used to build the image (only works if not
 * .dockerignore'd). If you're running a sekrit club fork, you can no-op this
 * function and set the BUILD_INFO env var manually, though I would prefer you
 * didn't set it to something misleading.
 */
async function setBuildInfo() {
  // Render .dockerignore's the .git directory but provides info in the env
  if (process.env.RENDER) {
    const sha = process.env.RENDER_GIT_COMMIT?.slice(0, 7) || "unknown SHA";
    const branch = process.env.RENDER_GIT_BRANCH || "unknown branch";
    const repo = process.env.RENDER_GIT_REPO_SLUG || "unknown repo";
    const buildInfo = `${sha} (${branch}@${repo})`;
    process.env.BUILD_INFO = buildInfo;
    logger.info({ build: buildInfo }, "Got build info from Render config.");
    return;
  }

  try {
    // Ignore git's complaints about dubious directory ownership on Huggingface
    // (which evidently runs dockerized Spaces on Windows with weird NTFS perms)
    if (process.env.SPACE_ID) {
      childProcess.execSync("git config --global --add safe.directory /app");
    }

    const promisifyExec = (cmd: string) =>
      new Promise((resolve, reject) => {
        childProcess.exec(cmd, (err, stdout) =>
          err ? reject(err) : resolve(stdout)
        );
      });

    const promises = [
      promisifyExec("git rev-parse --short HEAD"),
      promisifyExec("git rev-parse --abbrev-ref HEAD"),
      promisifyExec("git config --get remote.origin.url"),
      promisifyExec("git status --porcelain"),
    ].map((p) => p.then((result: any) => result.toString().trim()));

    let [sha, branch, remote, status] = await Promise.all(promises);

    remote = remote.match(/.*[\/:]([\w-]+)\/([\w\-\.]+?)(?:\.git)?$/) || [];
    const repo = remote.slice(-2).join("/");
    status = status
      // ignore Dockerfile changes since that's how the user deploys the app
      .split("\n")
      .filter((line: string) => !line.endsWith("Dockerfile") && line);

    const changes = status.length > 0;

    const build = `${sha}${changes ? " (modified)" : ""} (${branch}@${repo})`;
    process.env.BUILD_INFO = build;
    logger.info({ build, status, changes }, "Got build info from Git.");
  } catch (error: any) {
    logger.error(
      {
        error,
        stdout: error.stdout?.toString(),
        stderr: error.stderr?.toString(),
      },
      "Failed to get commit SHA.",
      error
    );
    process.env.BUILD_INFO = "unknown";
  }
}

start();


code:
import { getTokenizer } from "@anthropic-ai/tokenizer";
import { Tiktoken } from "tiktoken/lite";

let encoder: Tiktoken;

export function init() {
  // they export a `countTokens` function too but it instantiates a new
  // tokenizer every single time and it is not fast...
  encoder = getTokenizer();
  return true;
}

export function getTokenCount(prompt: string, _model: string) {
  // Don't try tokenizing if the prompt is massive to prevent DoS.
  // 500k characters should be sufficient for all supported models.
  if (prompt.length > 500000) {
    return {
      tokenizer: "length fallback",
      token_count: 100000,
    };
  }

  return {
    tokenizer: "@anthropic-ai/tokenizer",
    token_count: encoder.encode(prompt.normalize("NFKC"), "all").length,
  };
}


code:
export { OpenAIPromptMessage } from "./openai";
export { init, countTokens } from "./tokenizer";


code:
import { Tiktoken } from "tiktoken/lite";
import cl100k_base from "tiktoken/encoders/cl100k_base.json";

let encoder: Tiktoken;

export function init() {
  encoder = new Tiktoken(
    cl100k_base.bpe_ranks,
    cl100k_base.special_tokens,
    cl100k_base.pat_str
  );
  return true;
}

// Tested against:
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb

export function getTokenCount(messages: any[], model: string) {
  const gpt4 = model.startsWith("gpt-4");

  const tokensPerMessage = gpt4 ? 3 : 4;
  const tokensPerName = gpt4 ? 1 : -1; // turbo omits role if name is present

  let numTokens = 0;

  for (const message of messages) {
    numTokens += tokensPerMessage;
    for (const key of Object.keys(message)) {
      {
        const value = message[key];
        // Break if we get a huge message or exceed the token limit to prevent
        // DoS.
        // 100k tokens allows for future 100k GPT-4 models and 500k characters
        // is just a sanity check
        if (value.length > 500000 || numTokens > 100000) {
          numTokens = 100000;
          return {
            tokenizer: "tiktoken (prompt length limit exceeded)",
            token_count: numTokens,
          };
        }

        numTokens += encoder.encode(message[key]).length;
        if (key === "name") {
          numTokens += tokensPerName;
        }
      }
    }
  }
  numTokens += 3; // every reply is primed with <|start|>assistant<|message|>
  return { tokenizer: "tiktoken", token_count: numTokens };
}

export type OpenAIPromptMessage = {
  name?: string;
  content: string;
  role: string;
};


code:
import { Request } from "express";
import { config } from "../config";
import {
  init as initClaude,
  getTokenCount as getClaudeTokenCount,
} from "./claude";
import {
  init as initOpenAi,
  getTokenCount as getOpenAITokenCount,
  OpenAIPromptMessage,
} from "./openai";

export async function init() {
  if (config.anthropicKey) {
    initClaude();
  }
  if (config.openaiKey) {
    initOpenAi();
  }
}

type TokenCountResult = {
  token_count: number;
  tokenizer: string;
  tokenization_duration_ms: number;
};
type TokenCountRequest = {
  req: Request;
} & (
  | { prompt: string; service: "anthropic" }
  | { prompt: OpenAIPromptMessage[]; service: "openai" }
);
export async function countTokens({
  req,
  service,
  prompt,
}: TokenCountRequest): Promise<TokenCountResult> {
  const time = process.hrtime();
  switch (service) {
    case "anthropic":
      return {
        ...getClaudeTokenCount(prompt, req.body.model),
        tokenization_duration_ms: getElapsedMs(time),
      };
    case "openai":
      return {
        ...getOpenAITokenCount(prompt, req.body.model),
        tokenization_duration_ms: getElapsedMs(time),
      };
    default:
      throw new Error(`Unknown service: ${service}`);
  }
}

function getElapsedMs(time: [number, number]) {
  const diff = process.hrtime(time);
  return diff[0] * 1000 + diff[1] / 1e6;
}


code:
import { Express } from "express-serve-static-core";
import { AIService, Key } from "../key-management/index";
import { User } from "../proxy/auth/user-store";

declare global {
  namespace Express {
    interface Request {
      key?: Key;
      /** Denotes the format of the user's submitted request. */
      inboundApi: AIService | "kobold";
      /** Denotes the format of the request being proxied to the API. */
      outboundApi: AIService;
      /** If the request comes from a RisuAI.xyz user, this is their token. */
      risuToken?: string;
      user?: User;
      isStreaming?: boolean;
      startTime: number;
      retryCount: number;
      queueOutTime?: number;
      onAborted?: () => void;
      proceed: () => void;
      heartbeatInterval?: NodeJS.Timeout;
      promptTokens?: number;
      outputTokens?: number;
      // TODO: remove later
      debug: Record<string, any>;
    }
  }
}


code: Index | Logout


code:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="<%= csrfToken %>">
    <title><%= title %></title>
    <style>
      .pagination {
        list-style-type: none;
        padding: 0;
      }
      .pagination li {
        display: inline-block;
      }
      .pagination li a {
        display: block;
        padding: 0.5em 1em;
        text-decoration: none;
      }
      .pagination li.active a {
        background-color: #58739c;
        color: #fff;
      }

      table {
        border-collapse: collapse;
        border: 1px solid #ccc;
      }
      table td, table th {
        border: 1px solid #ccc;
        padding: 0.25em 0.5em;
      }
      th.active {
        background-color: #e0e6f6;
      }
      td.actions {
        padding: 0;
        text-align: center;
      }
      td.actions a {
        text-decoration: none;
        padding: 0.5em;
        height: 100%;
        width: 100%;
      }
      td.actions:hover {
        background-color: #ccc;
      }

      @media (max-width: 600px) {
        table {
          width: 100%;
        }
        table td, table th {
          display: block;
          width: 100%;
        }
      }
    </style>
  </head>
  <body style="font-family: sans-serif; background-color: #f0f0f0; padding: 1em;">


code:
  Page Size
  <select id="pageSize" onchange="setPageSize(this.value)" style="margin-bottom: 1rem;">
    <option value="10" <% if (pageSize === 10) { %>selected<% } %>>10</option>
    <option value="20" <% if (pageSize === 20) { %>selected<% } %>>20</option>
    <option value="50" <% if (pageSize === 50) { %>selected<% } %>>50</option>
    <option value="100" <% if (pageSize === 100) { %>selected<% } %>>100</option>
    <option value="200" <% if (pageSize === 200) { %>selected<% } %>>200</option>
  </select>


code:
<%- include("../_partials/admin-header", { title: "Create User - OAI Reverse Proxy Admin" }) %>

Create User Token

<form action="/admin/manage/create-user" method="post">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <input type="submit" value="Create" />
</form>
<% if (newToken) { %>

Just created <%= recentUsers[0].token %>.

<% } %>

Recent Tokens

  <% recentUsers.forEach(function(user) { %>

<a href="/admin/manage/view-user/<%= user.token %>"><%= user.token %>

  <% }) %>

<%- include("../_partials/admin-footer") %>


code:
<%- include("../_partials/admin-header", { title: "Export Users - OAI Reverse Proxy Admin" }) %>

Export Users

  Export users to JSON. The JSON will be an array of objects under the key
  users. You can use this JSON to import users later.

<button onclick="exportUsers()">Export</button>
<%- include("../_partials/admin-footer") %>


code:
<%- include("../_partials/admin-header", { title: "Import Users - OAI Reverse Proxy Admin" }) %>

Import Users

  Import users from JSON. The JSON should be an array of objects under the key
  users. Each object should have the following fields:

token (required): a unique identifier for the user

ip (optional): IP addresses the user has connected from

    type (optional): either normal or
    special

    promptCount (optional): the number of times the user has sent a
    prompt

    tokenCount (optional): the number of tokens the user has
    consumed (not yet implemented)

    createdAt (optional): the timestamp when the user was created

    disabledAt (optional): the timestamp when the user was disabled

    disabledReason (optional): the reason the user was disabled

  If a user with the same token already exists, the existing user will be
  updated with the new values.

<form action="/admin/manage/import-users?_csrf=<%= csrfToken %>" method="post" enctype="multipart/form-data">
  <input type="file" name="users" />
  <input type="submit" value="Import" />
</form>
</form>
<% if (imported > 0) { %>

Imported <%= imported %> users.

<% } %>
<%- include("../_partials/admin-footer") %>


code:
<%- include("../_partials/admin-header", { title: "OAI Reverse Proxy Admin" }) %>

OAI Reverse Proxy Admin

<% if (!isPersistenceEnabled) { %>

  ⚠ Users will be lost when the server restarts because persistence is
  not configured.

Be sure to export your users and import them again after restarting the
  server if you want to keep them.

 See the
  user management documentation to learn how to set up persistence.

<% } %>

List Users

Create User

Import Users

Export Users

<%- include("../_partials/admin-footer") %>


code:
<%- include("../_partials/admin-header", { title: "Users - OAI Reverse Proxy Admin" }) %>

User Token List

    <input type="hidden" name="_csrf" value="<%= csrfToken %>" />

    <% if (users.length === 0) { %>

No users found.

    <% } else { %>

          Token
          <th <% if (sort.includes("ip")) { %>class="active"<% } %> >IPs
          <th <% if (sort.includes("promptCount")) { %>class="active"<% } %> >Prompts
          Type
          <th <% if (sort.includes("createdAt")) { %>class="active"<% } %> >Created (UTC)
          <th <% if (sort.includes("lastUsedAt")) { %>class="active"<% } %> >Last Used (UTC)
          Banned?

        <% users.forEach(function(user){ %>

            <a href="/admin/manage/view-user/<%= user.token %>"><%= user.token %>

          <%= user.ip.length %>
          <%= user.promptCount %>
          <%= user.type %>
          <%= user.createdAt %>
          <%= user.lastUsedAt ?? "never" %>

            <% if (user.disabledAt) { %>
            <a title="Unban" href="#" class="unban" data-token="<%= user.token %>">🔄
            <% } else { %>
            <a title="Ban" href="#" class="ban" data-token="<%= user.token %>">🚫
            <% } %>
          <%= user.disabledAt ? "Yes" : "No" %> <%= user.disabledReason ? `(${user.disabledReason})` : "" %>

        <% }); %>

      <% if (page > 1) { %>

<a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= page - 1 %>">«

      <% } %> <% for (var i = 1; i <= pageCount; i++) { %>
      <li <% if (i === page) { %>class="active"<% } %>><a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= i %>"><%= i %>

      <% } %> <% if (page < pageCount) { %>

<a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= page + 1 %>">»

      <% } %>

Showing <%= page * pageSize - pageSize + 1 %> to <%= users.length + page * pageSize - pageSize %> of <%= totalCount %> users.

    <%- include("../_partials/pagination") %>
    <% } %>

<%- include("../_partials/admin-footer") %>


code:
<%- include("../_partials/admin-header", { title: "Login" }) %>

Login

    <% if (failed) { %>

Please try again.

    <% } %>
    <form action="/admin/login" method="post">
      <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
      Admin Key
      <input type="password" name="token" />
      <input type="submit" value="Login" />
    </form>


code:
<%- include("../_partials/admin-header", { title: "View User - OAI Reverse Proxy Admin" }) %>

View User

      Key
      Value

      Token
      <%- user.token %>

      Type
      <%- user.type %>

      Prompt Count
      <%- user.promptCount %>

      Token Count
      <%- user.tokenCount %>

      Created At
      <%- user.createdAt %>

      Last Used At
      <%- user.lastUsedAt || "never" %>

      Disabled At
      <%- user.disabledAt %>

      Disabled Reason
      <%- user.disabledReason %>

      IPs

        Show all (<%- user.ip.length %>)

          <% user.ip.forEach((ip) => { %>

<%- ip %>

          <% }) %>

<%- include("../_partials/admin-footer") %>


module: CommonJS

moduleResolution: node

outDir: build


code:
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# IPython Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# Start.tw
src/config/start.tw

path: .gitignore


code:
# Ignore everything in this directory
*
# Except this file
!.gitignore


code:
#!/bin/bash

# Will add all *.tw files to StoryIncludes.
rm src/config/start.tw
cp src/config/start.tw.proto start.tw.tmp
find src -name '*.tw' -print >>start.tw.tmp
mv start.tw.tmp src/config/start.tw

HASH=`git log -n1 |grep commit | sed 's/commit //'`

./devTools/tweeGo/tweego -o bin/FC_pregmod.html src/config/start.tw

rm src/config/start.tw

path: compile


code:
#!/bin/bash

# Will add all *.tw files to StoryIncludes.
rm src/config/start.tw
cp src/config/start.tw.proto start.tw.tmp
find src -name '*.tw' -print >>start.tw.tmp
mv start.tw.tmp src/config/start.tw

HASH=`git log -n1 |grep -m1 commit | sed 's/commit //'`

./devTools/tweeGo/tweego -o bin/FC_pregmod_$HASH.html src/config/start.tw

rm src/config/start.tw

echo "FC_pregmod_$HASH.html" compilation finished.

path: compile-git


code:
@echo off
:: Free Cities Basic Compiler - Windows x86_64

:: Will add all *.tw files to StoryIncludes.
del src\config\start.tw
copy src\config\start.tw.proto start.tw.tmp >nul
>>start.tw.tmp (for /r "src" %%F in (*.tw) do echo %%F)
move start.tw.tmp src\config\start.tw >nul

CALL "%~dp0devTools\tweeGo\tweego.exe" -o "%~dp0bin/FC_pregmod.html" "%~dp0src\config\start.tw"

del src\config\start.tw
ECHO Done


code:
@echo off
:: Free Cities Basic Compiler - Windows x86_64
:: Will wait for keypress before terminating.

:: Will add all *.tw files to StoryIncludes.
del src\config\start.tw
copy src\config\start.tw.proto start.tw.tmp >nul
>>start.tw.tmp (for /r "src" %%F in (*.tw) do echo %%F)
move start.tw.tmp src\config\start.tw >nul

CALL "%~dp0devTools\tweeGo\tweego.exe" -o "%~dp0bin/FC_pregmod.html" "%~dp0src\config\start.tw"

del src\config\start.tw

ECHO Done
PAUSE


code:
:: slave generation widgets [nobr]

<<widget "NationalityToRace">>

<<if $fixedRace == 0>>

<<if ($activeSlave.nationality is "American")>>
	<<set $activeSlave.race to either("black", "middle eastern", "white", "white", "white", "latina", "latina", "asian", "amerindian", "mixed race")>>
<<elseif ($activeSlave.nationality is "Canadian")>>
	<<set $activeSlave.race to either("white", "white", "white", "white", "amerindian")>>
<<elseif ($activeSlave.nationality is "Puerto Rican")>>
	<<set $activeSlave.race to either("latina")>>
<<elseif ($activeSlave.nationality is "Cuban")>>
	<<set $activeSlave.race to either("latina", "black")>>
<<elseif ($activeSlave.nationality is "Haitian")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Jamaican")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Mexican")>>
	<<set $activeSlave.race to either("latina", "latina", "latina", "latina", "latina", "amerindian")>>
<<elseif ($activeSlave.nationality is "Dominican")>>
	<<set $activeSlave.race to either("mixed race", "mixed race", "mixed race", "mixed race", "mixed race", "mixed race", "mixed race", "white", "white", "black")>>
<<elseif ($activeSlave.nationality is "Peruvian")>>
	<<set $activeSlave.race to either("latina", "amerindian")>>
<<elseif ($activeSlave.nationality is "Venezuelan")>>
	<<set $activeSlave.race to either("latina")>>
<<elseif ($activeSlave.nationality is "Bolivian")>>
	<<set $activeSlave.race to either("latina", "amerindian")>>
<<elseif ($activeSlave.nationality is "Guatemalan")>>
	<<set $activeSlave.race to either("latina", "amerindian")>>
<<elseif ($activeSlave.nationality is "Brazilian")>>
	<<set $activeSlave.race to either("black", "latina", "mixed race", "mixed race", "amerindian", "white")>>
<<elseif ($activeSlave.nationality is "Argentinian")>>
	<<set $activeSlave.race to either("white", "latina", "latina")>>
<<elseif ($activeSlave.nationality is "Chilean")>>
	<<set $activeSlave.race to either("white", "latina", "latina", "latina")>>
<<elseif ($activeSlave.nationality is "Colombian")>>
	<<set $activeSlave.race to either("latina")>>
<<elseif ($activeSlave.nationality is "Egyptian")>>
	<<set $activeSlave.race to either("black", "middle eastern", "middle eastern", "middle eastern", "semitic")>>
<<elseif ($activeSlave.nationality is "Turkish")>>
	<<set $activeSlave.race to either("middle eastern", "middle eastern", "middle eastern", "semitic")>>
<<elseif ($activeSlave.nationality is "Iranian")>>
	<<set $activeSlave.race to either("indo-aryan", "indo-aryan", "indo-aryan", "semitic")>>
<<elseif ($activeSlave.nationality is "Armenian")>>
	<<set $activeSlave.race to either("indo-aryan", "semitic")>>
<<elseif ($activeSlave.nationality is "Israeli")>>
	<<set $activeSlave.race to either("white", "middle eastern", "semitic", "semitic")>>
<<elseif ($activeSlave.nationality is "Saudi")>>
	<<set $activeSlave.race to either("black", "asian", "middle eastern", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Moroccan")>>
	<<set $activeSlave.race to either("middle eastern", "middle eastern", "black")>>
<<elseif ($activeSlave.nationality is "Nigerian")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Kenyan")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Zimbabwean")>>
	<<set $activeSlave.race to either("black", "black", "black", "black", "white")>>
<<elseif ($activeSlave.nationality is "Ugandan")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Tanzanian")>>
	<<set $activeSlave.race to either("black", "black", "black", "semitic")>>
<<elseif ($activeSlave.nationality is "Ghanan")>>
	<<set $activeSlave.race to either("black", "black", "black", "semitic")>>
<<elseif ($activeSlave.nationality is "Congolese")>>
	<<set $activeSlave.race to either("black")>>
<<elseif ($activeSlave.nationality is "Ethiopian")>>
	<<set $activeSlave.race to either("black", "black", "black", "middle eastern", "semitic")>>
<<elseif ($activeSlave.nationality is "South African")>>
	<<set $activeSlave.race to either("black", "black", "black", "white")>>
<<elseif ($activeSlave.nationality is "Chinese")>>
	<<set $activeSlave.race to either("asian")>>
<<elseif ($activeSlave.nationality is "Korean")>>
	<<set $activeSlave.race to either("asian")>>
<<elseif ($activeSlave.nationality is "Japanese")>>
	<<set $activeSlave.race to either("asian")>>
<<elseif ($activeSlave.nationality is "Thai")>>
	<<set $activeSlave.race to either("asian", "asian", "malay")>>
<<elseif ($activeSlave.nationality is "Vietnamese")>>
	<<set $activeSlave.race to either("asian")>>
<<elseif ($activeSlave.nationality is "Indonesian")>>
	<<set $activeSlave.race to either("asian", "malay", "malay", "pacific islander")>>
<<elseif ($activeSlave.nationality is "Filipina")>>
	<<set $activeSlave.race to either("asian", "malay", "malay", "pacific islander")>>
<<elseif ($activeSlave.nationality is "Burmese")>>
	<<set $activeSlave.race to either("asian", "asian", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Nepalese")>>
	<<set $activeSlave.race to either("asian", "asian", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Uzbek")>>
	<<set $activeSlave.race to either("asian")>>
<<elseif ($activeSlave.nationality is "Afghan")>>
	<<set $activeSlave.race to either("indo-aryan", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Algerian")>>
	<<set $activeSlave.race to either("middle eastern")>>
<<elseif ($activeSlave.nationality is "Libyan")>>
	<<set $activeSlave.race to either("middle eastern")>>
<<elseif ($activeSlave.nationality is "Tunisian")>>
	<<set $activeSlave.race to either("middle eastern")>>
<<elseif ($activeSlave.nationality is "Lebanese")>>
	<<set $activeSlave.race to either("middle eastern", "semitic")>>
<<elseif ($activeSlave.nationality is "Jordanian")>>
	<<set $activeSlave.race to either("middle eastern", "semitic")>>
<<elseif ($activeSlave.nationality is "Emirati")>>
	<<set $activeSlave.race to either("middle eastern", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Omani")>>
	<<set $activeSlave.race to either("middle eastern", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Malian")>>
	<<set $activeSlave.race to either("black", "black", "black", "black", "black", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Sudanese")>>
	<<set $activeSlave.race to either("black", "black", "black", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Yemeni")>>
	<<set $activeSlave.race to either("black", "semitic", "middle eastern", "middle eastern", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Iraqi")>>
	<<set $activeSlave.race to either("semitic", "middle eastern", "middle eastern", "middle eastern", "middle eastern")>>
<<elseif ($activeSlave.nationality is "Indian")>>
	<<set $activeSlave.race to either("indo-aryan")>>
<<elseif ($activeSlave.nationality is "Malaysian")>>
	<<set $activeSlave.race to either("asian", "malay", "malay", "malay")>>
<<elseif ($activeSlave.nationality is "Kazakh")>>
	<<set $activeSlave.race to either("asian", "asian", "asian", "semitic", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Pakistani")>>
	<<set $activeSlave.race to either("indo-aryan", "indo-aryan", "indo-aryan", "semitic")>>
<<elseif ($activeSlave.nationality is "Bangladeshi")>>
	<<set $activeSlave.race to either("indo-aryan")>>
<<elseif ($activeSlave.nationality is "Belarusian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Russian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Ukrainian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Irish")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Icelandic")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Finnish")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Swiss")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Danish")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Norwegian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Austrian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Slovak")>>
	<<set $activeSlave.race to either("white", "white", "white", "white", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Dutch")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Belgian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Czech")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Serbian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Portuguese")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Hungarian")>>
	<<set $activeSlave.race to either("white", "white", "white", "white", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "Estonian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Polish")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Lithuanian")>>
	<<set $activeSlave.race to either("white")>>
<<elseif ($activeSlave.nationality is "Romanian")>>
	<<set $activeSlave.race to either("semitic", "white", "white", "white", "white", "indo-aryan")>>
<<elseif ($activeSlave.nationality is "German")>>
	<<set $activeSlave.race to either("black", "middle eastern", "white", "white", "white", "white", "white", "white")>>
<<elseif ($activeSlave.nationality is "Swedish")>>
	<<set $activeSlave.race to either("middle eastern", "white", "white", "white", "white")>>
<<elseif ($activeSlave.nationality is "French")>>
	<<set $activeSlave.race to either("black", "middle eastern", "white", "white", "white", "white", "white", "southern European")>>
<<elseif ($activeSlave.nationality is "Italian")>>
	<<set $activeSlave.race to either("middle eastern", "southern European", "southern European", "white", "white")>>
<<elseif ($activeSlave.nationality is "Greek")>>
	<<set $activeSlave.race to either("southern European")>>
<<elseif ($activeSlave.nationality is "Spanish")>>
	<<set $activeSlave.race to either("semitic", "southern European", "southern European")>>
<<elseif ($activeSlave.nationality is "British")>>
	<<set $activeSlave.race to either("indo-aryan", "white", "white", "white", "white", "white", "white", "white", "white", "white")>>
<<elseif ($activeSlave.nationality is "Scottish")>>
	<<set $activeSlave.race to either("middle eastern", "indo-aryan", "white", "white", "white", "white", "white", "white", "white")>>
<<elseif ($activeSlave.nationality is "Australian")>>
	<<set $activeSlave.race to either("white", "white", "black", "asian")>>
<<elseif ($activeSlave.nationality is "a New Zealander")>>
	<<set $activeSlave.race to either("white", "white", "white", "white", "pacific islander")>>
<</if>>

/% Begin mixed race rate adjustment. %/
/% Some countries are extremely ethnically homogeneous and unlikely to change soon. %/
<<if ($activeSlave.nationality is "Japanese")>>
	<<if random(1,100) > 98>>
		<<set $activeSlave.race to "mixed race">>
	<</if>>
<<elseif ($activeSlave.nationality is "Korean")>>
	<<if random(1,100) > 95>>
		<<set $activeSlave.race to "mixed race">>
	<</if>>
<<elseif ($activeSlave.nationality is "Polish") or ($activeSlave.nationality is "Romanian") or ($activeSlave.nationality is "Bulgarian") or ($activeSlave.nationality is "Lithuanian") or ($activeSlave.nationality is "Croatian")>>
	/% Croatia isn't an origin currently but the game might add it in the future. %/
	<<if random(1,100) > 98>>
		<<set $activeSlave.race to "mixed race">>
	<</if>>
<<elseif random(1,100) > 90>>
	/% Default rate. %/
	<<set $activeSlave.race to "mixed race">>
<</if>>
/% End mixed race rate adjustment. %/

<<else>>

<<switch $fixedRace>>
<<case "white">>
	<<set $activeSlave.nationality to $whiteNationalities.random()>>
<<case "asian">>
	<<set $activeSlave.nationality to $asianNationalities.random()>>
<<case "latina">>
	<<set $activeSlave.nationality to $latinaNationalities.random()>>
<<case "middle eastern">>
	<<set $activeSlave.nationality to $middleEasternNationalities.random()>>
<<case "black">>
	<<set $activeSlave.nationality to $blackNationalities.random()>>
<<case "indo-aryan">>
	<<set $activeSlave.nationality to $indoAryan"Nationalities.random()>>
<<case "pacific islander">>
	<<set $activeSlave.nationality to $pacificIslanderNationalities.random()>>
<<case "malay">>
	<<set $activeSlave.nationality to $malayNationalities.random()>>
<<case "amerindian">>
	<<set $activeSlave.nationality to $amerindianNationalities.random()>>
<<case "southern european">>
	<<set $activeSlave.nationality to $southernEuropean"Nationalities.random()>>
<<case "semitic">>
	<<set $activeSlave.nationality to $semiticNationalities.random()>>
<</switch>>
<<set $activeSlave.race = $fixedRace>>

<</if>>

<<if $arcologies[0].FSSupremacistLawME != 0>>
<<if $activeSlave.race == $arcologies[0].FSSupremacistRace>>
	<<set $activeSlave.race to "mixed race">>
<</if>>
<</if>>

<</widget>>

<<widget "NationalityToName">>

<<if ($activeSlave.nationality is "American")>>
	<<if $activeSlave.race is "black">>
		<<set $activeSlave.birthName to $africanAmericanSlaveNames.random()>>
	<<elseif $activeSlave.race is "latina">>
		<<set $activeSlave.birthName to $latinaSlaveNames.random()>>
	<<elseif $activeSlave.race is "asian">>
		<<set $activeSlave.birthName to $asianAmericanSlaveNames.random()>>
	<<elseif $activeSlave.race is "middle eastern">>
		<<set $activeSlave.birthName to $egyptianSlaveNames.random()>>
	<<else>>
		<<set $activeSlave.birthName to $whiteAmericanSlaveNames.random()>>
	<</if>>
<<case "Canadian">>
	<<set $activeSlave.birthName to $canadianSlaveNames.random()>>
<<case "Mexican">>
	<<set $activeSlave.birthName to $mexicanSlaveNames.random()>>
<<case "Dominican">>
	<<set $activeSlave.birthName to $dominicanSlaveNames.random()>>
<<case "Puerto Rican">>
	<<set $activeSlave.birthName to $puertoRicanSlaveNames.random()>>
<<case "Haitian">>
	<<set $activeSlave.birthName to $haitianSlaveNames.random()>>
<<case "Jamaican">>
	<<set $activeSlave.birthName to $jamaicanSlaveNames.random()>>
<<case "Cuban">>
	<<set $activeSlave.birthName to $cubanSlaveNames.random()>>
<<case "Guatemalan">>
	<<set $activeSlave.birthName to $guatemalanSlaveNames.random()>>
<<case "Chilean">>
	<<set $activeSlave.birthName to $chileanSlaveNames.random()>>
<<case "Peruvian">>
	<<set $activeSlave.birthName to $peruvianSlaveNames.random()>>
<<case "Bolivian">>
	<<set $activeSlave.birthName to $bolivianSlaveNames.random()>>
<<case "Venezuelan">>
	<<set $activeSlave.birthName to $venezuelanSlaveNames.random()>>
<<case "Belarusian">>
	<<set $activeSlave.birthName to $belarusianSlaveNames.random()>>
<<case "Russian">>
	<<set $activeSlave.birthName to $russianSlaveNames.random()>>
<<case "Ukrainian">>
	<<set $activeSlave.birthName to $ukrainianSlaveNames.random()>>
<<case "Italian">>
	<<set $activeSlave.birthName to $italianSlaveNames.random()>>
<<case "Spanish">>
	<<set $activeSlave.birthName to $spanishSlaveNames.random()>>
<<case "British">>
	<<set $activeSlave.birthName to $britishSlaveNames.random()>>
<<case "Scottish">>
	<<set $activeSlave.birthName to $scottishSlaveNames.random()>>
<<case "French">>
	<<set $activeSlave.birthName to $frenchSlaveNames.random()>>
<<case "German">>
	<<set $activeSlave.birthName to $germanSlaveNames.random()>>
<<case "Lithuanian">>
	<<set $activeSlave.birthName to $lithuanianSlaveNames.random()>>
<<case "Norwegian">>
	<<set $activeSlave.birthName to $norwegianSlaveNames.random()>>
<<case "Slovak">>
	<<set $activeSlave.birthName to $slovakSlaveNames.random()>>
<<case "Danish">>
	<<set $activeSlave.birthName to $danishSlaveNames.random()>>
<<case "Dutch">>
	<<set $activeSlave.birthName to $dutchSlaveNames.random()>>
<<case "Austrian">>
	<<set $activeSlave.birthName to $austrianSlaveNames.random()>>
<<case "Swiss">>
	<<set $activeSlave.birthName to $swissSlaveNames.random()>>
<<case "Serbian">>
	<<set $activeSlave.birthName to $serbianSlaveNames.random()>>
<<case "Belgian">>
	<<set $activeSlave.birthName to $belgianSlaveNames.random()>>
<<case "Czech">>
	<<set $activeSlave.birthName to $czechSlaveNames.random()>>
<<case "Portuguese">>
	<<set $activeSlave.birthName to $portugueseSlaveNames.random()>>
<<case "Swedish">>
	<<set $activeSlave.birthName to $swedishSlaveNames.random()>>
<<case "Romanian">>
	<<set $activeSlave.birthName to $romanianSlaveNames.random()>>
<<case "Hungarian">>
	<<set $activeSlave.birthName to $hungarianSlaveNames.random()>>
<<case "Estonian">>
	<<set $activeSlave.birthName to $estonianSlaveNames.random()>>
<<case "Irish">>
	<<set $activeSlave.birthName to $irishSlaveNames.random()>>
<<case "Icelandic">>
	<<set $activeSlave.birthName to $icelandicSlaveNames.random()>>
<<case "Finnish">>
	<<set $activeSlave.birthName to $finnishSlaveNames.random()>>
<<case "Greek">>
	<<set $activeSlave.birthName to $greekSlaveNames.random()>>
<<case "Polish">>
	<<set $activeSlave.birthName to $polishSlaveNames.random()>>
<<case "Brazilian">>
	<<set $activeSlave.birthName to $brazilianSlaveNames.random()>>
<<case "Egyptian">>
	<<set $activeSlave.birthName to $egyptianSlaveNames.random()>>
<<case "Colombian">>
	<<set $activeSlave.birthName to $colombianSlaveNames.random()>>
<<case "Argentinian">>
	<<set $activeSlave.birthName to $argentinianSlaveNames.random()>>
<<case "Turkish">>
	<<set $activeSlave.birthName to $turkishSlaveNames.random()>>
<<case "Iranian">>
	<<set $activeSlave.birthName to $iranianSlaveNames.random()>>
<<case "Armenian">>
	<<set $activeSlave.birthName to $armenianSlaveNames.random()>>
<<case "Israeli">>
	<<set $activeSlave.birthName to $israeliSlaveNames.random()>>
<<case "Saudi">>
	<<set $activeSlave.birthName to $saudiSlaveNames.random()>>
<<case "South African">>
	<<if $activeSlave.race is "black">>
		<<set $activeSlave.birthName to $blackSouthAfricanSlaveNames.random()>>
	<<else>>
		<<set $activeSlave.birthName to $whiteSouthAfricanSlaveNames.random()>>
	<</if>>
<<case "Nigerian">>
	<<set $activeSlave.birthName to $nigerianSlaveNames.random()>>
<<case "Congolese">>
	<<set $activeSlave.birthName to $congoleseSlaveNames.random()>>
<<case "Kenyan">>
	<<set $activeSlave.birthName to $kenyanSlaveNames.random()>>
<<case "Tanzanian">>
	<<set $activeSlave.birthName to $tanzanianSlaveNames.random()>>
<<case "Zimbabwean">>
	<<if $activeSlave.race == "white">>
		<<set $activeSlave.birthName to $britishSlaveNames.random()>>
	<<else>>
		<<set $activeSlave.birthName to $zimbabweanSlaveNames.random()>>
	<</if>>
<<case "Ghanan">>
	<<set $activeSlave.birthName to $ghananSlaveNames.random()>>
<<case "Ugandan">>
	<<set $activeSlave.birthName to $ugandanSlaveNames.random()>>
<<case "Ethiopian">>
	<<set $activeSlave.birthName to $ethiopianSlaveNames.random()>>
<<case "Moroccan">>
	<<set $activeSlave.birthName to $moroccanSlaveNames.random()>>
<<case "Chinese">>
	<<set $activeSlave.birthName to $chineseSlaveNames.random()>>
<<case "Korean">>
	<<set $activeSlave.birthName to $koreanSlaveNames.random()>>
<<case "Thai">>
	<<set $activeSlave.birthName to $thaiSlaveNames.random()>>
<<case "Vietnamese">>
	<<set $activeSlave.birthName to $vietnameseSlaveNames.random()>>
<<case "Japanese">>
	<<set $activeSlave.birthName to $japaneseSlaveNames.random()>>
<<case "Indonesian">>
	<<set $activeSlave.birthName to $indonesianSlaveNames.random()>>
<<case "Filipina">>
	<<set $activeSlave.birthName to $filipinaSlaveNames.random()>>
<<case "Bangladeshi">>
	<<set $activeSlave.birthName to $bangladeshiSlaveNames.random()>>
<<case "Malaysian">>
	<<set $activeSlave.birthName to $malaysianSlaveNames.random()>>
<<case "Uzbek">>
	<<set $activeSlave.birthName to $uzbekSlaveNames.random()>>
<<case "Afghan">>
	<<set $activeSlave.birthName to $afghanSlaveNames.random()>>
<<case "Nepalese">>
	<<set $activeSlave.birthName to $nepaleseSlaveNames.random()>>
<<case "Burmese">>
	<<set $activeSlave.birthName to $burmeseSlaveNames.random()>>
<<case "Iraqi">>
	<<set $activeSlave.birthName to $iraqiSlaveNames.random()>>
<<case "Yemeni">>
	<<set $activeSlave.birthName to $yemeniSlaveNames.random()>>
<<case "Sudanese">>
	<<set $activeSlave.birthName to $sudaneseSlaveNames.random()>>
<<case "Algerian">>
	<<set $activeSlave.birthName to $algerianSlaveNames.random()>>
<<case "Tunisian">>
	<<set $activeSlave.birthName to $tunisianSlaveNames.random()>>
<<case "Libyan">>
	<<set $activeSlave.birthName to $libyanSlaveNames.random()>>
<<case "Omani">>
	<<set $activeSlave.birthName to $omaniSlaveNames.random()>>
<<case "Malian">>
	<<set $activeSlave.birthName to $malianSlaveNames.random()>>
<<case "Jordanian">>
	<<set $activeSlave.birthName to $jordanianSlaveNames.random()>>
<<case "Lebanese">>
	<<set $activeSlave.birthName to $lebaneseSlaveNames.random()>>
<<case "Emirati">>
	<<set $activeSlave.birthName to $emiratiSlaveNames.random()>>
<<case "Kazakh">>
	<<set $activeSlave.birthName to $kazakhSlaveNames.random()>>
<<case "Pakistani">>
	<<set $activeSlave.birthName to $pakistaniSlaveNames.random()>>
<<case "Indian">>
	<<set $activeSlave.birthName to $indianSlaveNames.random()>>
<<case "Australian">>
	<<set $activeSlave.birthName to $australianSlaveNames.random()>>
<<case "a New Zealander">>
	<<set $activeSlave.birthName to $newZealanderSlaveNames.random()>>
<<default>>
	<<set $activeSlave.birthName to $whiteAmericanSlaveNames.random()>>
<</switch>>

<</widget>>

<<widget "NationalityToAccent">>

<<set $seed to either(0,1,1,2,2,2,3,3,3,3)>>

<<if ($activeSlave.nationality is "American")>>
	<<if $activeSlave.race is "black">>
	<<if $language is "English">>
		<<set $activeSlave.accent to 0>>
	<<else>>
		<<set $activeSlave.accent to either(0,1,1,2,2,2,3,3,3,3)>>
	<</if>>
	<<elseif $activeSlave.race is "latina">>
	<<if $language is "English">>
		<<set $activeSlave.accent to 0>>
	<<elseif $language is "Spanish">>
		<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
		<<set $activeSlave.accent to $seed>>
	<</if>>
	<<elseif $activeSlave.race is "asian">>
	<<if $language is "English">>
		<<set $activeSlave.accent to 0>>
	<<elseif $language is "Chinese">>
		<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
		<<set $activeSlave.accent to $seed>>
	<</if>>
	<<elseif $activeSlave.race is "middle eastern">>
	<<if $language is "English">>
		<<set $activeSlave.accent to 0>>
	<<elseif $language is "Arabic">>
		<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
		<<set $activeSlave.accent to $seed>>
	<</if>>
	<<else>>
	<<if $language is "English">>
		<<set $activeSlave.accent to 0>>
	<<else>>
		<<set $activeSlave.accent to $seed>>
	<</if>>
	<</if>>
<<elseif ($activeSlave.nationality is "Canadian")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Mexican")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Dominican")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Puerto Rican")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Haitian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Jamaican")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Cuban")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Guatemalan")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Chilean")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Peruvian")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Bolivian")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Venezuelan")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Russian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Ukrainian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Italian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Spanish")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "British")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Scottish")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 2>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "French")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "German")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Lithuanian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Belarusian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "French")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "German")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Lithuanian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Norwegian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Slovak")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Danish")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Dutch")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Austrian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Swiss")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Serbian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Belgian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Czech")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Portuguese")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Swedish")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Romanian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Hungarian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Estonian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Irish")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Icelandic")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Finnish")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Greek")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Polish")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Brazilian")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Egyptian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Colombian")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Argentinian")>>
	<<if $language is "Spanish">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Turkish")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Iranian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Armenian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Israeli")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Saudi")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "South African")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Nigerian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Congolese")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Kenyan")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Tanzanian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Zimbabwean")>>
	<<if $language is "English" && $activeSlave.race == "white">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Ghanan")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Ugandan")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Ethiopian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Moroccan")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Chinese")>>
	<<if $language is "Chinese">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Korean")>>
	<<if $language is "Chinese">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Thai")>>
	<<if $language is "Chinese">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Vietnamese")>>
	<<if $language is "Chinese">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Japanese")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Indonesian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Filipina")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Bangladeshi")>>
	<<if $language is "Indian">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Malaysian")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Uzbek")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Afghan")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Nepalese")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Burmese")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Iraqi")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Yemeni")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Sudanese")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Algerian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Tunisian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Libyan")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Omani")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Malian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Jordanian")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Lebanese")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Emirati")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Kazakh")>>
	<<set $activeSlave.accent to $seed>>
<<elseif ($activeSlave.nationality is "Pakistani")>>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to either(0,1,2,3)>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Indian")>>
	<<if $language is "Indian">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "Australian")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif ($activeSlave.nationality is "a New Zealander")>>
	<<if $language is "English">>
	<<set $activeSlave.accent to 1>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif $activeSlave.nationality is "Roman Revivalist">>
	<<if $language is "Latin">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif $activeSlave.nationality is "Ancient Egyptian Revivalist">>
	<<if $language is "Ancient Egyptian">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif $activeSlave.nationality is "Edo Revivalist">>
	<<if $language is "Japanese">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif $activeSlave.nationality is "Arabian Revivalist">>
	<<if $language is "Arabic">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<elseif $activeSlave.nationality is "Ancient Chinese Revivalist">>
	<<if $language is "Chinese">>
	<<set $activeSlave.accent to 0>>
	<<else>>
	<<set $activeSlave.accent to $seed>>
	<</if>>
<<else>>
	<<set $activeSlave.accent to $seed>>
<</if>>

<<if $activeSlave.nationality is $language>>
	<<set $activeSlave.accent to 0>>
<</if>>

<</widget>>

path: devNotes/Slave Generation Widgets.txt


code:
########################################################################################################################
#
# sugarcube-2.py
#
# Copyright (c) 2013-2017 Thomas Michael Edwards <tmedwards@motoslave.net>. All rights reserved.
# Use of this source code is governed by a Simplified BSD License which can be found in the LICENSE file.
#
########################################################################################################################

import os, os.path, header
from collections import OrderedDict

class Header (header.Header):

	def filesToEmbed(self):
		userLibPath = self.path + os.sep + 'userlib.js'
		if os.path.isfile(userLibPath):
			return OrderedDict([
				('"USER_LIB"', userLibPath)
			])
		else:
			return OrderedDict()

	def storySettings(self):
		return "SugarCube 2.x does not support the StorySettings special passage.\n\nInstead, you should use its configuration object, config.\n    See: http://www.motoslave.net/sugarcube/2/docs/config-object.html"

	def isEndTag(self, name, tag):
		return (name == ('/' + tag) or name == ('end' + tag))

	def nestedMacros(self):
		return [
				# standard macros
				'append',
				'button',
				'capture',
				'createplaylist',
				'for',
				'if',
				'link',
				'linkappend',
				'linkprepend',
				'linkreplace',
				'nobr',
				'prepend',
				'repeat',
				'replace',
				'script',
				'switch',
				'silently',
				'timed',
				'widget',
				# deprecated macros
				'click'
			]

	def passageTitleColor(self, passage):
		additionalSpecialPassages = [
				'PassageDone',
				'PassageFooter',
				'PassageHeader',
				'PassageReady',
				'StoryBanner',
				'StoryCaption',
				'StoryShare'
			]
		if passage.isStylesheet():
			return ((111, 49, 83), (234, 123, 184))
		elif passage.isScript():
			return ((89, 66, 28), (226, 170, 80))
		elif ('widget' in passage.tags):
			return ((80, 106, 26), (134, 178, 44))
		elif passage.isInfoPassage() or (passage.title in additionalSpecialPassages):
			return ((28, 89, 74), (41, 214, 113))
		elif passage.title == 'Start':
			return ('#4ca333', '#4bdb24')

	def passageChecks(self):
		return super(Header, self).passageChecks()


code:
How to mod (basic doc):

1. All sources now in the src subdir, in separate files. 1 passage = 1 file.

2. Special files and dir's:
    - src/config 		- configuration of the story is here.
    - src/config/start.tw 	- contains list of .tw passage files, regenerated automatic, by building scripts. Do not change by hands. (origial passage Start from pregmod renamed and moved to src/events/intro/introSummary.tw)
    - src/js/storyJS.tw		- special passage with [script] tag - contain all native JavaScript from pregmod.
    - devTools/tweeGo/targets/sugarcube-2/userlib.js - on original FC JS moved here (I deleted it after moving JS to storyJS.tw). Compare to storyJS.tw but do not copy file here. May conflict with src/js/storyJS.tw if copied.
    - src/pregmod 		- I put all pregmod-only passages here.
    - .gitignore 		- special file for git - to ignore some files. For example - compilation results.

3. Compilation:

    Windows:
    Run compile.bat - result will be file bin/FC_pregmod.html
    Second run of compile.but will overwrite bin/FC_pregmod.html without prompt.

    Linux:
    Ensure executable permission on file "devTools/tweeGo/tweego" (not tweego.exe!)
    Ensure executable permission on file "compile"
    In the root dir of sources (where you see src, devTools, bin...) run command "./compile" from console
    compile-git will produce the same result file but with current commit hash in filename.

    Mac:
    Don't supported directly (I don't have access to Mac for testing).
    But potentially can be used linux compilation script if you download tweego for mac from here: https://bitbucket.org/tmedwards/tweego/downloads/ and replace linux executable with mac executable in ./devTools/tweeGo/ folder. This is not tested though.

4. Simple comparing and merging with original FC:

    Use meld tool. Place folder FreeCities (original FC sources tree) near FreeCitiesPregmod (this sources tree) and use command:
    meld FreeCities FreeCitiesPregmod
    or just select these folders in meld's GUI.

5. All modders will be wery grateful if any, who make some changes to game, with .html file also post his/her resulting src folder tree.

6. For contributors to pregmod: if you don't use git, then you need to post yours version of src folder tree, not just produced FC_pregmod.html file!!! This html file can't be reverted to proper sources, and useless as contribution!

7. Git workflow:
    - Master branch is pregmod-master. Only Pregmodder can add something to it directly. Always contain his last public changes.
    - pregmod-dev - branch with experimental code mainly by pregmodfan.
    - Any contributions will be placed in separate branches like pregmod-mod-<something> (if it's ready to merge with master complete feature/mod) or pregmod-contrib-<something> if it's partial work until contributions is reviewed.

    Typical cycle with git:
	1. Make account on gitgud if you don't have usable one.
	2. Fork main repository through gitgud interface. (Or pull changes from main repo if you already have fork.)
	3. Clone your fork to local machine witn git client  (Or pull changes if already cloned.)
	4. Make you changes as you like, commit, and push result into your forked repository (with git client).
	5. Make merge request through gitgud interface.