mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 17:48:46 -10:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19952f6c6c | ||
|
|
6618d3d2f8 | ||
|
|
60ddce0ad7 | ||
|
|
173cd2517f | ||
|
|
6006ef5208 | ||
|
|
afc198a82c | ||
|
|
2796eda3cb | ||
|
|
a5b9216fc6 | ||
|
|
5fbd0997a4 | ||
|
|
8bf3b01d16 | ||
|
|
ff2bb8ee95 | ||
|
|
690d600df8 | ||
|
|
e7a897aa4d | ||
|
|
050c76b721 | ||
|
|
7bb0c77bb5 |
No files matched your search
@@ -210,3 +210,28 @@ jobs:
|
||||
DATACORDER_USER: ${{ secrets.DATACORDER_USER }}
|
||||
DATACORDER_SSHPASS: ${{ secrets.DATACORDER_PASSWORD }}
|
||||
run: $GITHUB_WORKSPACE/ci/linux/upload_dist_package.sh nightly
|
||||
|
||||
post_builds:
|
||||
# Post nightlies if at least one job was successful
|
||||
if: ${{ needs.linux_zip == "success" || needs.windows_zip == "success" }}
|
||||
name: Post builds on Nebula and the forums
|
||||
needs: [linux_zip, windows_zip]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
name: Checkout
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
- name: Install Python dependencies
|
||||
run: pip install -r ci/post/requirements.txt
|
||||
- name: Post builds
|
||||
env:
|
||||
INDIEGAMES_USER: ${{ secrets.INDIEGAMES_USER }}
|
||||
INDIEGAMES_SSHPASS: ${{ secrets.INDIEGAMES_PASSWORD }}
|
||||
NEBULA_USER: ${{ secrets.NEBULA_USER }}
|
||||
NEBULA_PASSWORD: ${{ secrets.NEBULA_PASSWORD }}
|
||||
HLP_API: ${{ secrets.HLP_API }}
|
||||
HLP_KEY: ${{ secrets.HLP_KEY }}
|
||||
LINUX_RESULT: ${{ needs.linux_zip.result }}
|
||||
WINDOWS_RESULT: ${{ needs.windows_zip.result }}
|
||||
run: python ci/post/main.py nightly
|
||||
@@ -263,3 +263,26 @@ jobs:
|
||||
asset_path: ${{ steps.generate_package.outputs.debug_path }}
|
||||
asset_name: ${{ steps.generate_package.outputs.debug_name }}
|
||||
asset_content_type: ${{ steps.generate_package.outputs.debug_mime }}
|
||||
|
||||
post_builds:
|
||||
name: Post builds on Nebula and the forums
|
||||
needs: [linux_zip, windows_zip]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
name: Checkout
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
- name: Install Python dependencies
|
||||
run: pip install -r ci/post/requirements.txt
|
||||
- name: Post builds
|
||||
env:
|
||||
INDIEGAMES_USER: ${{ secrets.INDIEGAMES_USER }}
|
||||
INDIEGAMES_SSHPASS: ${{ secrets.INDIEGAMES_PASSWORD }}
|
||||
NEBULA_USER: ${{ secrets.NEBULA_USER }}
|
||||
NEBULA_PASSWORD: ${{ secrets.NEBULA_PASSWORD }}
|
||||
HLP_API: ${{ secrets.HLP_API }}
|
||||
HLP_KEY: ${{ secrets.HLP_KEY }}
|
||||
LINUX_RESULT: ${{ needs.linux_zip.result }}
|
||||
WINDOWS_RESULT: ${{ needs.windows_zip.result }}
|
||||
run: python ci/post/main.py release
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Push a new nightly tag
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
if: github.repository == 'scp-fs2open/fs2open.github.com'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
git config --global user.email '[email protected]'
|
||||
git config --global user.name SirKnightly
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Create tag
|
||||
run: git tag "nightly_$(date '+%Y%m%d')_$(git rev-parse --short HEAD)"
|
||||
- name: Push tag
|
||||
run: git push --tags
|
||||
@@ -0,0 +1,128 @@
|
||||
import re
|
||||
from ftplib import FTP, error_perm
|
||||
from typing import List, Tuple, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from util import retry_multi, GLOBAL_TIMEOUT
|
||||
|
||||
class ReleaseFile:
|
||||
def __init__(self, name, url, group, subgroup=None, mirrors=None):
|
||||
if mirrors is None:
|
||||
mirrors = []
|
||||
self.mirrors = mirrors
|
||||
self.subgroup = subgroup
|
||||
self.group = group
|
||||
self.url = url
|
||||
self.name = name
|
||||
|
||||
self.base_url = "/".join(url.split('/')[0:-1]) + "/"
|
||||
self.filename = url.split('/')[-1]
|
||||
|
||||
# A list of tuples of (filename, hash)
|
||||
self.content_hashes = None
|
||||
|
||||
self.hash = None
|
||||
self.size = 0
|
||||
|
||||
|
||||
class SourceFile:
|
||||
def __init__(self, name, url, group):
|
||||
self.group = group
|
||||
self.url = url
|
||||
self.name = name
|
||||
|
||||
|
||||
def get_release_files(tag_name, config) -> Tuple[List[ReleaseFile], Dict[str, SourceFile]]:
|
||||
@retry_multi(5)
|
||||
def execute_request(path):
|
||||
headers = {
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
url = "https://api.github.com" + path
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=GLOBAL_TIMEOUT)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
build_group_regex = re.compile("fs2_open_.*-builds-([^.-]*)(-([^.]*))?.*")
|
||||
source_file_regex = re.compile("fs2_open_.*-source-([^.]*)?.*")
|
||||
|
||||
response = execute_request(
|
||||
"/repos/{}/{}/releases/tags/{}".format(config["github"]["user"], config["github"]["repo"], tag_name))
|
||||
|
||||
binary_files = []
|
||||
source_files = {}
|
||||
for asset in response["assets"]:
|
||||
url = asset["browser_download_url"]
|
||||
name = asset["name"]
|
||||
|
||||
group_match = build_group_regex.match(name)
|
||||
|
||||
if group_match is not None:
|
||||
platform = group_match.group(1)
|
||||
# x64 is the Visual Studio name but for consistency we need Win64
|
||||
if platform == "x64":
|
||||
platform = "Win64"
|
||||
|
||||
binary_files.append(ReleaseFile(name, url, platform, group_match.group(3)))
|
||||
else:
|
||||
group_match = source_file_regex.match(name)
|
||||
|
||||
if group_match is None:
|
||||
continue
|
||||
|
||||
group = group_match.group(1)
|
||||
|
||||
source_files[group] = SourceFile(name, url, group)
|
||||
|
||||
return binary_files, source_files
|
||||
|
||||
|
||||
def get_ftp_files(build_type, tag_name, config):
|
||||
tag_regex = re.compile("nightly_(.*)")
|
||||
build_group_regex = re.compile("nightly_.*-builds-([^.]+).*")
|
||||
|
||||
files = []
|
||||
try:
|
||||
with FTP(config["ftp"]["host"], config["ftp"]["user"], config["ftp"]["pass"]) as ftp:
|
||||
version_str = tag_regex.match(tag_name).group(1)
|
||||
|
||||
path_template = config["ftp"]["path"]
|
||||
path = path_template.format(type=build_type, version=version_str)
|
||||
file_entries = list(ftp.mlsd(path, ["type"]))
|
||||
|
||||
for entry in file_entries:
|
||||
if entry[1]["type"] == "file":
|
||||
files.append(entry[0])
|
||||
except error_perm:
|
||||
print("Received permanent FTP error!")
|
||||
return []
|
||||
|
||||
out_data = []
|
||||
for file in files:
|
||||
file_match = build_group_regex.match(file)
|
||||
if file_match is None:
|
||||
print("Ignoring non nightly file '{}'".format(file))
|
||||
continue
|
||||
|
||||
group_match = file_match.group(1)
|
||||
primary_url = None
|
||||
mirrors = []
|
||||
|
||||
# x64 is the name Visual Studio uses but Win64 works better for us since that gets displayed in the nightly post
|
||||
if "x64" in group_match:
|
||||
group_match = group_match.replace("x64", "Win64")
|
||||
|
||||
for mirror in config["ftp"]["mirrors"]:
|
||||
download_url = mirror.format(type=build_type, version=version_str, file=file)
|
||||
if primary_url is None:
|
||||
primary_url = download_url
|
||||
else:
|
||||
mirrors.append(download_url)
|
||||
|
||||
out_data.append(ReleaseFile(file, primary_url, group_match, None, mirrors))
|
||||
|
||||
return out_data
|
||||
@@ -0,0 +1,82 @@
|
||||
from itertools import groupby
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
import semantic_version
|
||||
from mako.template import Template
|
||||
|
||||
from file_list import ReleaseFile
|
||||
|
||||
|
||||
class FileGroup:
|
||||
def __init__(self, name, files: List[ReleaseFile]):
|
||||
self.files = files
|
||||
self.name = name
|
||||
|
||||
if len(files) == 1:
|
||||
self.mainFile = files[0]
|
||||
self.subFiles = {}
|
||||
else:
|
||||
self.mainFile = None
|
||||
subFiles = []
|
||||
for file in files:
|
||||
# We only have subcategories for Windows where SSE2 is the main group
|
||||
if file.subgroup == "SSE2":
|
||||
self.mainFile = file
|
||||
else:
|
||||
subFiles.append(file)
|
||||
|
||||
self.subFiles = dict(((x[0], next(x[1])) for x in groupby(subFiles, lambda f: f.subgroup)))
|
||||
|
||||
|
||||
|
||||
class ForumAPI:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
def create_post(self, title, content, board):
|
||||
resp = requests.post(self.config["hlp"]["api"], data={
|
||||
"api_key": self.config["hlp"]["key"],
|
||||
"board": str(board),
|
||||
"subject": title,
|
||||
"body": content
|
||||
})
|
||||
|
||||
if resp.text != "OK":
|
||||
print("Post failed!")
|
||||
|
||||
def post_nightly(self, date, revision, files, log, success):
|
||||
print("Posting nightly thread...")
|
||||
|
||||
title = "Nightly: {} - Revision {}".format(date, revision)
|
||||
|
||||
template = Template(filename=self.config["templates"]["nightly"])
|
||||
rendered = template.render_unicode(**{
|
||||
"date": date,
|
||||
"revision": revision,
|
||||
"files": files,
|
||||
"log": log,
|
||||
"success": success
|
||||
})
|
||||
|
||||
print("Creating post...")
|
||||
self.create_post(title, rendered, self.config["nightly"]["hlp_board"])
|
||||
|
||||
def post_release(self, date, version: semantic_version.Version, groups, sources):
|
||||
print("Posting release thread...")
|
||||
|
||||
title = "Release: {}".format(version)
|
||||
|
||||
template = Template(
|
||||
filename=self.config["templates"]["release"].format(major=version.major, minor=version.minor),
|
||||
module_directory='/tmp/mako_modules')
|
||||
rendered = template.render_unicode(**{
|
||||
"date": date,
|
||||
"version": version,
|
||||
"groups": groups,
|
||||
"sources": sources
|
||||
}).strip("\n")
|
||||
|
||||
print("Creating post...")
|
||||
|
||||
self.create_post(title, rendered, self.config["release"]["hlp_board"])
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
import hashlib
|
||||
import tarfile
|
||||
import traceback
|
||||
import zipfile
|
||||
from tempfile import NamedTemporaryFile
|
||||
from zipfile import ZipFile
|
||||
|
||||
import requests
|
||||
import sys
|
||||
from mako.template import Template
|
||||
|
||||
from file_list import ReleaseFile
|
||||
from util import GLOBAL_TIMEOUT
|
||||
|
||||
|
||||
def _download_file(url, dest_file, session):
|
||||
print("Downloading " + url)
|
||||
# NOTE the stream=True parameter
|
||||
r = session.get(url, stream=True, timeout=GLOBAL_TIMEOUT)
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
dest_file.write(chunk)
|
||||
dest_file.flush()
|
||||
|
||||
|
||||
def _gen_hash(fileobj, hash_alg):
|
||||
h = hashlib.new(hash_alg)
|
||||
while True:
|
||||
data = fileobj.read(4096)
|
||||
|
||||
if not data:
|
||||
break
|
||||
|
||||
h.update(data)
|
||||
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def get_hashed_file_list(file: ReleaseFile, session: requests.Session = None, hash_alg: str = "sha256"):
|
||||
if session is None:
|
||||
session = requests.Session()
|
||||
|
||||
with NamedTemporaryFile('w+b', suffix=file.filename) as local_file:
|
||||
_download_file(file.url, local_file, session)
|
||||
|
||||
filename = local_file.name
|
||||
hash_list = []
|
||||
|
||||
local_file.seek(0)
|
||||
file.hash = _gen_hash(local_file, hash_alg)
|
||||
file.size = os.stat(filename).st_size
|
||||
|
||||
try:
|
||||
if tarfile.is_tarfile(filename):
|
||||
with tarfile.open(filename) as archive:
|
||||
for entry in archive:
|
||||
if not entry.isfile():
|
||||
continue
|
||||
|
||||
print("Computing hash for " + entry.name)
|
||||
fileobj = archive.extractfile(entry)
|
||||
hash_list.append((entry.path, _gen_hash(fileobj, hash_alg)))
|
||||
elif zipfile.is_zipfile(filename):
|
||||
with ZipFile(filename) as archive:
|
||||
for entry in archive.infolist():
|
||||
# Python 3.6 has is_dir but that version is relatively new so we'll use the "safe" version here
|
||||
if entry.filename.endswith('/'):
|
||||
continue
|
||||
|
||||
print("Computing hash for " + entry.filename)
|
||||
with archive.open(entry) as fileobj:
|
||||
hash_list.append((entry.filename, _gen_hash(fileobj, hash_alg)))
|
||||
else:
|
||||
raise NotImplementedError("Unsupported archive type!")
|
||||
except:
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
return
|
||||
|
||||
file.content_hashes = hash_list
|
||||
|
||||
|
||||
def render_installer_config(version, groups, config):
|
||||
template = Template(filename=config["templates"]["installer"], module_directory='/tmp/mako_modules')
|
||||
return template.render(**{
|
||||
"version": version,
|
||||
"groups": groups,
|
||||
}).strip("\n")
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from subprocess import check_output
|
||||
from datetime import datetime
|
||||
from itertools import groupby
|
||||
|
||||
import semantic_version
|
||||
|
||||
import file_list
|
||||
import installer
|
||||
import nebula
|
||||
import forum
|
||||
|
||||
|
||||
MAJOR_VERSION_PATTERN = re.compile("set_if_not_defined\(FSO_VERSION_MAJOR (\d+)\)")
|
||||
MINOR_VERSION_PATTERN = re.compile("set_if_not_defined\(FSO_VERSION_MINOR (\d+)\)")
|
||||
BUILD_VERSION_PATTERN = re.compile("set_if_not_defined\(FSO_VERSION_BUILD (\d+)\)")
|
||||
LOG_FORMAT = """
|
||||
------------------------------------------------------------------------%n
|
||||
commit %h%nAuthor: %an <%ad>%n
|
||||
Commit: %cn <%cd>%n%n %s
|
||||
"""
|
||||
DATEFORMAT_VERSION = "%Y%m%d"
|
||||
DATEFORMAT_FORUM = "%d %B %Y"
|
||||
|
||||
|
||||
def _match_version_number(text, regex):
|
||||
match = regex.search(text)
|
||||
return int(match.group(1))
|
||||
|
||||
|
||||
def get_source_version(date_version):
|
||||
with open(os.path.join("..", "..", "cmake", "version.cmake"), "r") as f:
|
||||
filetext = f.read()
|
||||
|
||||
major = _match_version_number(filetext, MAJOR_VERSION_PATTERN)
|
||||
minor = _match_version_number(filetext, MINOR_VERSION_PATTERN)
|
||||
build = _match_version_number(filetext, BUILD_VERSION_PATTERN)
|
||||
|
||||
return semantic_version.Version("{}.{}.{}-{}".format(major, minor, build, date_version))
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
config = {
|
||||
"github": {
|
||||
"user": "scp-fs2open",
|
||||
"repo": "fs2open.github.com",
|
||||
},
|
||||
"ftp": {
|
||||
"host": "scp.indiegames.us",
|
||||
"user": os.environ["INDIEGAMES_USER"],
|
||||
"pass": os.environ["INDIEGAMES_SSHPASS"],
|
||||
"path": "public_html/builds/{type}/{version}/",
|
||||
"mirrors": (
|
||||
"https://porphyrion.feralhosting.com/datacorder/builds/{type}/{version}/{file}",
|
||||
"http://scp.indiegames.us/builds/{type}/{version}/{file}",
|
||||
),
|
||||
},
|
||||
"nebula": {
|
||||
"user": os.environ["NEBULA_USER"],
|
||||
"password": os.environ["NEBULA_PASSWORD"],
|
||||
},
|
||||
"hlp": {
|
||||
"api": os.environ["HLP_API"],
|
||||
"key": os.environ["HLP_KEY"],
|
||||
},
|
||||
"templates": {
|
||||
"nightly": "nightly.mako",
|
||||
"release": "release.mako",
|
||||
},
|
||||
"nightly": {
|
||||
"hlp_board": 173,
|
||||
},
|
||||
"release": {
|
||||
"hlp_board": 50,
|
||||
},
|
||||
}
|
||||
|
||||
if sys.argv[1] not in ("release", "nightly"):
|
||||
print("ERROR: Invalid release mode %s passed. Expected release or nightly!" % sys.argv[1])
|
||||
sys.exit(1)
|
||||
|
||||
tag_name = os.path.basename(os.environ["GITHUB_REF"])
|
||||
date = datetime.now()
|
||||
version = get_source_version(date.strftime(DATEFORMAT_VERSION))
|
||||
success = os.environ["LINUX_RESULT"] == "success" and os.environ["WINDOWS_RESULT"] == "success"
|
||||
|
||||
tags = check_output(("git", "for-each-ref", "--sort=-taggerdate", "--format", "%(tag)", "refs/tags")).splitlines()
|
||||
previous_tag = None
|
||||
found = False
|
||||
|
||||
for tag in tags:
|
||||
if found:
|
||||
if tag.startswith(sys.argv[1] + "_"):
|
||||
previous_tag = tag
|
||||
break
|
||||
elif tag == tag_name:
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
print("ERROR: Couldn't find tag %s in repo!" % tag_name)
|
||||
sys.exit(1)
|
||||
|
||||
if not previous_tag:
|
||||
print("ERROR: Could not find a previous tag for %s!" % tag_name)
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == "release":
|
||||
if not tag_name.startswith("release_"):
|
||||
print("ERROR: Invalid tag name detected. Expected release_ prefix for release builds but found %s (full ref = %s)" % tag_name, os.environ["GITHUB_REF"])
|
||||
sys.exit(1)
|
||||
|
||||
files, sources = file_list.get_release_files(tag_name, config)
|
||||
|
||||
print("Hashing files")
|
||||
for file in files:
|
||||
installer.get_hashed_file_list(file)
|
||||
|
||||
# Construct the file groups
|
||||
groups = dict(((x[0], file_list.FileGroup(x[0], list(x[1]))) for x in groupby(files, lambda g: g.group)))
|
||||
|
||||
print(installer.render_installer_config(version, groups, config))
|
||||
|
||||
nebula.submit_release(
|
||||
nebula.render_nebula_release(version, "rc" if version.prerelease else "stable", "files", config),
|
||||
config)
|
||||
|
||||
fapi = forum.ForumAPI(config)
|
||||
fapi.post_release(date.strftime(DATEFORMAT_FORUM), version, groups, sources)
|
||||
else:
|
||||
if not tag_name.startswith("nightly_"):
|
||||
print("ERROR: Invalid tag name detected. Expected nightly_ prefix for nightly builds but found %s (full ref = %s)" % tag_name, os.environ["GITHUB_REF"])
|
||||
sys.exit(1)
|
||||
|
||||
print("Retrieving file list")
|
||||
files = file_list.get_ftp_files("nightly", tag_name, config)
|
||||
|
||||
print("Hashing files")
|
||||
for file in files:
|
||||
installer.get_hashed_file_list(file)
|
||||
|
||||
nebula.submit_release(nebula.render_nebula_release(version, "nightly", files, config), config)
|
||||
|
||||
log = check_output(("git", "log", "%s^..%s^" % (previous_tag, tag_name), "--no-merges", "--stat", "--pretty=format:\"%s\"" % LOG_FORMAT.strip()))
|
||||
|
||||
fapi = forum.ForumAPI(config)
|
||||
fapi.post_nightly(date.strftime(DATEFORMAT_FORUM), os.environ["GITHUB_SHA"], files, log, success)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,240 @@
|
||||
import os.path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
|
||||
from util import retry_multi, GLOBAL_TIMEOUT
|
||||
|
||||
metadata = {
|
||||
'type': 'engine',
|
||||
'title': 'FSO',
|
||||
'notes': '',
|
||||
'banner': 'https://fsnebula.org/storage/0d/e7/bf64bcdea9a9c115969cfb784e1ca457d24a7c2da4fc6f213521c3bb6abb.png',
|
||||
'screenshots': [],
|
||||
'videos': [],
|
||||
'first_release': '2017-09-24',
|
||||
'cmdline': '',
|
||||
'mod_flag': ['FSO'],
|
||||
'tile': 'https://fsnebula.org/storage/ec/cc/0bf23e028c26d5175ff52d003bff85b0a17b0ddfc1130d65bdf6d36f6324.png',
|
||||
'logo': None,
|
||||
'release_thread': None,
|
||||
'description': '',
|
||||
'id': 'FSO',
|
||||
'packages': [],
|
||||
}
|
||||
|
||||
platforms = {
|
||||
'Linux': 'linux',
|
||||
'MacOSX': 'macosx',
|
||||
'Win32-SSE2': 'windows',
|
||||
'Win64-SSE2': 'windows',
|
||||
'Win32-AVX': 'windows',
|
||||
'Win64-AVX': 'windows'
|
||||
}
|
||||
|
||||
envs = {
|
||||
'Linux': 'linux && x86_64', # Linux only has 64bit builds
|
||||
'MacOSX': 'macosx',
|
||||
'Win32-SSE2': 'windows',
|
||||
'Win64-SSE2': 'windows && x86_64',
|
||||
'Win32-AVX': 'windows && avx',
|
||||
'Win64-AVX': 'windows && avx && x86_64'
|
||||
}
|
||||
|
||||
subdirs = {
|
||||
'Win32-SSE2': 'x86',
|
||||
'Win64-SSE2': 'x64',
|
||||
'Win32-AVX': 'x86_avx',
|
||||
'Win64-AVX': 'x64_avx'
|
||||
}
|
||||
|
||||
|
||||
def render_nebula_release(version, stability, files, config):
|
||||
meta = metadata.copy()
|
||||
meta['version'] = str(version)
|
||||
meta['stability'] = stability # This can be one of ('stable', 'rc', 'nightly')
|
||||
|
||||
for file in files:
|
||||
if file.content_hashes is None:
|
||||
# The extraction probably failed which is why we don't have any hashes
|
||||
continue
|
||||
|
||||
group = file.group
|
||||
|
||||
# release.py sets subgroup but nightly.py doesn't which means we have to normalize group here.
|
||||
if file.subgroup:
|
||||
group += '-' + file.subgroup
|
||||
|
||||
pkg = {
|
||||
'name': group,
|
||||
'notes': '',
|
||||
'is_vp': False,
|
||||
'files': [{
|
||||
'dest': platforms[group] + '/' + subdirs.get(group, ''),
|
||||
'filesize': file.size,
|
||||
'checksum': ['sha256', file.hash],
|
||||
'urls': [file.url] + file.mirrors,
|
||||
'filename': file.name
|
||||
}],
|
||||
'environment': envs.get(group),
|
||||
'filelist': [],
|
||||
'status': 'required',
|
||||
'folder': None,
|
||||
'dependencies': [],
|
||||
'executables': []
|
||||
}
|
||||
|
||||
for fn, checksum in file.content_hashes:
|
||||
if group in subdirs:
|
||||
dest_fn = subdirs[group] + '/' + fn
|
||||
else:
|
||||
dest_fn = fn
|
||||
|
||||
# If the group is a known platform then we put the binaries into a separate subfolder for each platform
|
||||
if group in platforms:
|
||||
dest_fn = platforms[group] + '/' + dest_fn
|
||||
|
||||
pkg['filelist'].append({
|
||||
'orig_name': fn,
|
||||
'checksum': ('sha256', checksum),
|
||||
'archive': file.name,
|
||||
'filename': dest_fn
|
||||
})
|
||||
|
||||
if group == 'Linux' and fn.endswith('.AppImage'):
|
||||
if 'qtfred' in fn:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'QtFRED Debug'
|
||||
else:
|
||||
label = 'QtFRED'
|
||||
|
||||
else:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'Fast Debug'
|
||||
else:
|
||||
label = None
|
||||
|
||||
props = {
|
||||
"x64": True, # All Linux builds are 64-bit
|
||||
"sse2": True, # Linux builds are forced to compile with SSE2 but not AVX
|
||||
"avx": False,
|
||||
"avx2": False,
|
||||
}
|
||||
|
||||
pkg['executables'].append({
|
||||
'file': dest_fn,
|
||||
'label': label,
|
||||
'properties': props,
|
||||
})
|
||||
elif group == 'MacOSX' and fn.startswith(os.path.basename(fn) + '.app/'):
|
||||
if 'qtfred' in fn:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'QtFRED Debug'
|
||||
else:
|
||||
label = 'QtFRED'
|
||||
else:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'Fast Debug'
|
||||
else:
|
||||
label = None
|
||||
|
||||
props = {
|
||||
"x64": True, # All Mac builds are 64-bit
|
||||
"sse2": True, # There are no forced options on mac but 64-bit always implies SSE2 so we set that here
|
||||
"avx": False,
|
||||
"avx2": False,
|
||||
}
|
||||
|
||||
pkg['executables'].append({
|
||||
'file': dest_fn,
|
||||
'label': label,
|
||||
'properties': props,
|
||||
})
|
||||
elif group.startswith('Win') and fn.endswith('.exe'):
|
||||
if 'fred2' in fn:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'FRED2 Debug'
|
||||
else:
|
||||
label = 'FRED2'
|
||||
|
||||
elif 'qtfred' in fn:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'QtFRED Debug'
|
||||
else:
|
||||
label = 'QtFRED'
|
||||
|
||||
else:
|
||||
if '-FASTDBG' in fn:
|
||||
label = 'Fast Debug'
|
||||
else:
|
||||
label = None
|
||||
|
||||
props = {
|
||||
"x64": "x64" in fn,
|
||||
"sse2": "SSE2" in fn or "AVX" in fn, # AVX implies SSE2
|
||||
"avx": "AVX" in fn, # This conveniently also covers the AVX2 case since AVX2 implies AVX
|
||||
"avx2": "AVX2" in fn,
|
||||
}
|
||||
|
||||
pkg['executables'].append({
|
||||
'file': dest_fn,
|
||||
'label': label,
|
||||
'properties': props,
|
||||
})
|
||||
|
||||
meta["packages"].append(pkg)
|
||||
|
||||
return meta
|
||||
|
||||
|
||||
@retry_multi(5)
|
||||
def nebula_request(session, kind, path, **kwargs):
|
||||
uri = "https://fsnebula.org/api/1/{}".format(path)
|
||||
|
||||
request_args = {
|
||||
"timeout": GLOBAL_TIMEOUT
|
||||
}
|
||||
request_args.update(kwargs)
|
||||
|
||||
return session.request(kind, uri, **request_args)
|
||||
|
||||
|
||||
def submit_release(meta, config):
|
||||
try:
|
||||
with requests.Session() as session:
|
||||
print('Logging into Nebula...')
|
||||
result = nebula_request(session, 'post', 'login', data={
|
||||
'user': config['nebula']['user'],
|
||||
'password': config['nebula']['password']
|
||||
})
|
||||
|
||||
if result.status_code != 200:
|
||||
print('Login failed!')
|
||||
return False
|
||||
|
||||
data = result.json()
|
||||
if not data['result']:
|
||||
print('Login failed!')
|
||||
return False
|
||||
|
||||
print('Submitting release...')
|
||||
result = nebula_request(session, 'post', 'mod/release', headers={
|
||||
'X-KN-TOKEN': data['token']
|
||||
}, json=meta)
|
||||
|
||||
if result.status_code != 200:
|
||||
print('Request failed!')
|
||||
return False
|
||||
|
||||
data = result.json()
|
||||
if not data['result']:
|
||||
print('ERROR: ' + data['reason'])
|
||||
return False
|
||||
|
||||
print('Success!')
|
||||
return True
|
||||
except Exception:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||
return False
|
||||
@@ -0,0 +1,27 @@
|
||||
Here is the nightly for ${date} - Revision ${revision}
|
||||
|
||||
% if not success:
|
||||
[b][color=red]At least one of the nightly builds failed![/color][/b]
|
||||
% endif
|
||||
% for file in files:
|
||||
|
||||
|
||||
Group: ${file.group}
|
||||
[url=${file.url}]${file.name}[/url] \
|
||||
%for mirror in file.mirrors:
|
||||
%if loop.first:
|
||||
(\
|
||||
%endif
|
||||
[url=${mirror}]Mirror[/url]\
|
||||
%if loop.last:
|
||||
)\
|
||||
%else:
|
||||
, \
|
||||
%endif
|
||||
%endfor
|
||||
|
||||
% endfor
|
||||
|
||||
[code]
|
||||
${log}
|
||||
[/code]
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
<%def name="build(file)">[url=${file.url}]${file.filename}[/url]</%def>
|
||||
|
||||
[b]Change log:[/b] (chronologically ordered)
|
||||
[b]Bugfixes and behavior changes:[/b] Check these first if something breaks (most of these changes can be reverted / disabled).
|
||||
[list]
|
||||
[li]Wrap Docking getTime in a timestamp ([gh]#3701[/gh])[/li]
|
||||
[li]expand some sexp documentation ([gh]#3671[/gh])[/li]
|
||||
[li]Fix warnings concerning ballistic weapons ([gh]#3683[/gh])[/li]
|
||||
[li]Initialize team visibility before using it ([gh]#3695[/gh])[/li]
|
||||
[li]Actually fix the goal's target being sent ([gh]#3665[/gh])[/li]
|
||||
[li]Safety for replace-texture ([gh]#3694[/gh])[/li]
|
||||
[li]Fix issue 3685 ([gh]#3688[/gh])[/li]
|
||||
[li]Exclude dead subsys from targeting ([gh]#3614[/gh])[/li]
|
||||
[li]Fix scripted kills ([gh]#3679[/gh])[/li]
|
||||
[li]Fix homing child weapons in niche circumstances ([gh]#3661[/gh])[/li]
|
||||
[/list]
|
||||
|
||||
[b]Deprecations:[/b]
|
||||
[list][li]None[/li][/list]
|
||||
|
||||
Deprecations are a mechanism in FSO where a certain feature or aspect of the engine has changed or is no longer supported. Since this would normally break existing mods we have the mod table feature "[tt]$Target Version:[/tt]" with which a mod can specify what version of FSO it was developed with. The features listed above will be removed or changed when the target version of a mod is at least the version released in this post.
|
||||
|
||||
[size=5pt]Previous [url=https://www.hard-light.net/forums/index.php?topic=97293.0]21.0 Release Thread[/url][/size]
|
||||
|
||||
Launchers, if you don't have one already:
|
||||
[b]All platforms: [/b] For every day use, we recommend [url=https://www.hard-light.net/forums/index.php?topic=94068.0]Knossos[/url], an integrated solution for downloading and launching mods.
|
||||
|
||||
[hidden=Alternative Launchers]
|
||||
Cross-platform: [url=http://www.hard-light.net/forums/index.php?topic=89162]wxLauncher 0.12.x Test Build[/url] (ongoing project for a unified launcher, you should upgrade to the latest RC/test build if you have not yet)
|
||||
[b]Important:[/b] For best compatibility with FSO 3.8 and later you should use at least wxLauncher 0.12.
|
||||
|
||||
Windows: [url=http://scp.fsmods.net/files/Launcher55g.zip]Launcher 5.5g[/url] ([url=http://scp.indiegames.us/builds/Launcher55g.zip]Mirror[/url]) ([url=http://www.mediafire.com/?wdvzn7hhhzh418m]Mirror[/url]) Not compatible with Windows 8+, use wxLauncher above
|
||||
OS X: Soulstorm's [url=http://www.hard-light.net/forums/index.php/topic,51391.0.html]OS X Launcher 3.0[/url]
|
||||
Linux: [url=http://www.hard-light.net/forums/index.php/topic,53206.0.html]YAL[/url] or [url=http://www.hard-light.net/wiki/index.php/Fs2_open_on_Linux/Graphics_Settings]by hand[/url] or whatever you can figure out.[/hidden]
|
||||
|
||||
[img]http://scp.indiegames.us/img/windows-icon.png[/img] [color=green][size=12pt]Windows (32/64-bit)[/size][/color]
|
||||
[size=8pt]Compiled using GitHub Actions on Windows Server 2019 (10.0.17763), Visual Studio Enterprise 2019[/size]
|
||||
|
||||
[b]64-bit:[/b] ${build(groups["Win64"].mainFile)}
|
||||
|
||||
[b]32-bit:[/b] ${build(groups["Win32"].mainFile)}
|
||||
[size=8pt]This one is based on the SSE2 Optimizations from the MSVC Compiler.[/size]
|
||||
|
||||
[hidden=Alternative builds]
|
||||
|
||||
[b]64-bit AVX:[/b] ${build(groups["Win64"].subFiles["AVX"])}
|
||||
[size=8pt]This one is based on the AVX Optimizations from the MSVC Compiler (fastest build if your CPU supports AVX instructions).[/size]
|
||||
|
||||
|
||||
[b]32-bit AVX:[/b] ${build(groups["Win32"].subFiles["AVX"])}
|
||||
[size=8pt]This one is based on the AVX Optimizations from the MSVC Compiler.[/size]
|
||||
|
||||
[b]What are those SSE, SSE2 and AVX builds I keep seeing everywhere?[/b]
|
||||
[url=http://www.hard-light.net/forums/index.php?topic=65628.0]Your answer is in this topic.[/url]
|
||||
Don't want to deal with that? Use [url=https://www.hard-light.net/forums/index.php?topic=94068.0]Knossos[/url] and it will download the best build specifically for your PC!
|
||||
[/hidden]
|
||||
|
||||
[img]http://scp.indiegames.us/img/linux-icon.png[/img] [color=green][size=12pt]Linux 64-bit[/size][/color]
|
||||
[size=8pt]Compiled with Ubuntu 16.04 LTS 64-bit, GCC 5[/size]
|
||||
${build(groups["Linux"].mainFile)}
|
||||
|
||||
These builds use a mechanism called [url=http://appimage.org/]AppImage[/url] which should allow these builds to run on most Linux distributions. However, we recommend that you compile your own builds which will result in less issues.
|
||||
Alternatively, if there is a package in your software repository then you should use that. If you are the maintainer of such a package for a distribution then let us know and we will include that here.
|
||||
|
||||
|
||||
[img]http://scp.indiegames.us/img/mac-icon.png[/img] [color=green][size=12pt]OS X[/size][/color]
|
||||
[b][color=red]Not available[/color][/b] We recently lost access to our Mac CI environment which we usually used for compiling these builds so for the time being, there will be no builds for this OS.
|
||||
|
||||
[hidden=TrackIR Users]
|
||||
[size=12pt]Important!![/size]
|
||||
An external DLL is required for FSO to use TrackIR functions. The following DLL is simply unpacked in to your main FreeSpace2 root dir.
|
||||
TrackIR is only supported on Windows.
|
||||
[url=http://www.mediafire.com/download.php?4zw024zrh44etse]TrackIR SCP DLL[/url] ([url=http://scp.fsmods.net/builds/scptrackir.zip]Mirror[/url]) ([url=http://scp.indiegames.us/builds/scptrackir.zip]Mirror[/url])[/hidden]
|
||||
|
||||
Known issues:
|
||||
[list]
|
||||
[li][url=https://github.com/scp-fs2open/fs2open.github.com/issues]Github issues[/url] and [url=https://github.com/scp-fs2open/fs2open.github.com/pulls]pending pull requests[/url][/li]
|
||||
[li]See the list of [url=http://scp.indiegames.us/mantis/search.php?project_id=1&status_id%5B%5D=10&status_id%5B%5D=20&status_id%5B%5D=30&status_id%5B%5D=40&status_id%5B%5D=50&priority_id%5B%5D=40&priority_id%5B%5D=50&priority_id%5B%5D=60&sticky_issues=on&sortby=last_updated&dir=DESC&per_page=200&hide_status_id=-2]Fix for next release[/url] bugs - mark a bug as an elevated priority (high, urgent, immediate) to get it included in that filter.[/li]
|
||||
[/list]
|
||||
@@ -0,0 +1,3 @@
|
||||
Mako==1.1.5
|
||||
requests==2.26.0
|
||||
semantic-version==2.8.5
|
||||
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from requests import RequestException
|
||||
|
||||
GLOBAL_TIMEOUT = 30
|
||||
|
||||
|
||||
def retry_multi(max_retries):
|
||||
"""
|
||||
Retry a function `max_retries` times.
|
||||
This has been copied from https://stackoverflow.com/a/23892489
|
||||
"""
|
||||
|
||||
def retry(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
num_retries = 0
|
||||
while num_retries <= max_retries:
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
break
|
||||
except RequestException:
|
||||
if num_retries == max_retries:
|
||||
raise
|
||||
num_retries += 1
|
||||
time.sleep(5)
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
return retry
|
||||
|
||||
|
||||
def expand_config_vars(config):
|
||||
config["git"]["repo"] = os.path.expandvars(config["git"]["repo"])
|
||||
+2
-2
@@ -7,8 +7,8 @@ if (EXISTS "${PROJECT_SOURCE_DIR}/version_override.cmake")
|
||||
endif()
|
||||
|
||||
set_if_not_defined(FSO_VERSION_MAJOR 21)
|
||||
set_if_not_defined(FSO_VERSION_MINOR 3)
|
||||
set_if_not_defined(FSO_VERSION_BUILD 0)
|
||||
set_if_not_defined(FSO_VERSION_MINOR 4)
|
||||
set_if_not_defined(FSO_VERSION_BUILD 1)
|
||||
|
||||
# A numeric revision e.g. the CI build number
|
||||
set_if_not_defined(FSO_VERSION_REVISION 0)
|
||||
|
||||
@@ -405,7 +405,6 @@ typedef struct ai_info {
|
||||
|
||||
|
||||
union {
|
||||
float lead_scale; // Amount to lead current opponent by.
|
||||
float stay_near_distance; // Distance to stay within for AIM_STAY_NEAR mode.
|
||||
};
|
||||
|
||||
|
||||
+1
-2
@@ -11438,7 +11438,7 @@ void ai_dock()
|
||||
// calculate time until animations elapse
|
||||
int time1 = model_anim_get_time_type(shipp, AnimationTriggerType::Docked, docker_index);
|
||||
int time2 = model_anim_get_time_type(goal_shipp, AnimationTriggerType::Docked, dockee_index);
|
||||
aip->mode_time = MAX(time1, time2);
|
||||
aip->mode_time = timestamp(MAX(time1, time2));
|
||||
}
|
||||
|
||||
// if not enough time has passed, just wait
|
||||
@@ -14865,7 +14865,6 @@ void init_ai_object(int objnum)
|
||||
aip->danger_weapon_objnum = -1;
|
||||
aip->danger_weapon_signature = -1;
|
||||
|
||||
aip->lead_scale = 0.0f;
|
||||
aip->last_hit_target_time = Missiontime;
|
||||
aip->last_hit_time = Missiontime;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "playerman/player.h"
|
||||
#include "scripting/scripting.h"
|
||||
#include "ship/ship.h"
|
||||
#include "ship/awacs.h"
|
||||
#include "weapon/weapon.h"
|
||||
|
||||
// Just a reference to this struct being for looking up a ship's team in TVT
|
||||
@@ -187,6 +188,9 @@ void ai_post_process_mission()
|
||||
object *objp;
|
||||
int i;
|
||||
|
||||
// make sure team visibility is updated first
|
||||
awacs_process();
|
||||
|
||||
// Check ships in player starting wings. Those ships should follow these rules:
|
||||
// (1) if they have no orders, they should get a form on my wing order
|
||||
// (2) if they have an order, they are free to act on it.
|
||||
|
||||
@@ -1485,6 +1485,11 @@ void hud_do_lock_indicators(float frametime)
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !wip->wi_flags[Weapon::Info_Flags::Multilock_target_dead_subsys] && lock_slot->subsys != nullptr && lock_slot->subsys->current_hits <= 0.0f) {
|
||||
ship_clear_lock(lock_slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
hud_lock_determine_lock_point(lock_slot);
|
||||
|
||||
hud_lock_check_if_target_in_lock_cone(lock_slot, wip);
|
||||
|
||||
+26
-7
@@ -4304,19 +4304,38 @@ void HudGaugeLeadSight::render(float /*frametime*/)
|
||||
}
|
||||
}
|
||||
|
||||
// hud_cease_subsystem_targeting() will cease targeting the current targets subsystems
|
||||
//
|
||||
void hud_cease_subsystem_targeting(bool print_message)
|
||||
{
|
||||
int ship_index;
|
||||
|
||||
ship_index = Objects[Player_ai->target_objnum].instance;
|
||||
if ( ship_index < 0 )
|
||||
Assertion(Player_ai->target_objnum < MAX_OBJECTS, "Invalid player target objnum");
|
||||
|
||||
if (Player_ai->target_objnum < 0) {
|
||||
// Nothing selected
|
||||
if (print_message) {
|
||||
HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR("No target selected.", 322));
|
||||
snd_play(gamesnd_get_game_sound(GameSounds::TARGET_FAIL));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Objects[Player_ai->target_objnum].type != OBJ_SHIP) {
|
||||
// Object isn't a ship, so it doesn't have any subsystems
|
||||
if (print_message) {
|
||||
snd_play(gamesnd_get_game_sound(GameSounds::TARGET_FAIL));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ship_index = Objects[Player_ai->target_objnum].instance;
|
||||
|
||||
Assertion(ship_index >= 0 && ship_index < MAX_SHIPS, "Invalid ship index");
|
||||
Assertion(Player_num >= 0 && Player_num < MAX_PLAYERS, "Invalid player number");
|
||||
|
||||
Ships[ship_index].last_targeted_subobject[Player_num] = NULL;
|
||||
Player_ai->targeted_subsys = NULL;
|
||||
Player_ai->targeted_subsys_parent = -1;
|
||||
|
||||
if ( print_message ) {
|
||||
HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR( "Deactivating sub-system targeting", 324));
|
||||
}
|
||||
@@ -4325,14 +4344,14 @@ void hud_cease_subsystem_targeting(bool print_message)
|
||||
hud_lock_reset();
|
||||
}
|
||||
|
||||
// hud_cease_targeting() will cease targeting main target and subsystem
|
||||
// does not turn off auto-targeting by default
|
||||
void hud_cease_targeting(bool deliberate)
|
||||
{
|
||||
set_target_objnum( Player_ai, -1 );
|
||||
// Turn off subsys targeting before we invalidate the player target_objnum...
|
||||
hud_cease_subsystem_targeting(false);
|
||||
set_target_objnum(Player_ai, -1);
|
||||
|
||||
if (deliberate) {
|
||||
// Player wants to stop targeting, so turn off auto-target so we don't immediately aquire another target
|
||||
Players[Player_num].flags &= ~PLAYER_FLAGS_AUTO_TARGETING;
|
||||
HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR("Deactivating targeting system", 325));
|
||||
}
|
||||
|
||||
+15
-2
@@ -113,8 +113,21 @@ void hud_target_random_ship();
|
||||
|
||||
void hud_target_next_subobject();
|
||||
void hud_target_prev_subobject();
|
||||
void hud_cease_subsystem_targeting(bool print_message = true);
|
||||
void hud_cease_targeting(bool deliberate = false);
|
||||
|
||||
/**
|
||||
* @brief Cease targeting subsystems
|
||||
*
|
||||
* @param[in] print_message If true, provide feedback to the player
|
||||
*/
|
||||
void hud_cease_subsystem_targeting(bool print_message = true);
|
||||
|
||||
/**
|
||||
* @brief Cease targeting objects and their subsystems.
|
||||
*
|
||||
* @param[in] deliberate If true, turn off auto-targeting and provide feedback to player
|
||||
*/
|
||||
void hud_cease_targeting(bool deliberate = false);
|
||||
|
||||
void hud_restore_subsystem_target(ship* shipp);
|
||||
vec3d* get_subsystem_world_pos(object* parent_obj, ship_subsys* subsys, vec3d* world_pos);
|
||||
void hud_target_change_check();
|
||||
|
||||
@@ -1393,9 +1393,13 @@ int multi_oo_pack_data(net_player *pl, object *objp, ushort oo_flags, ubyte *dat
|
||||
if ((aip->wp_list != nullptr) && (aip->wp_list->get_waypoints().size() > aip->wp_index)) {
|
||||
target_signature = Objects[aip->wp_list->get_waypoints().at(aip->wp_index).get_objnum()].net_signature;
|
||||
}
|
||||
} // send the target signature.
|
||||
else if (aip->target_objnum != -1) {
|
||||
target_signature = Objects[Ai_info[shipp->ai_index].target_objnum].net_signature;
|
||||
} // send the target signature. 2021 Version!
|
||||
else if ((aip->goals[0].target_name != nullptr) && strlen(aip->goals[0].target_name) != 0) {
|
||||
|
||||
int instance = ship_name_lookup(aip->goals[0].target_name);
|
||||
if (instance > -1) {
|
||||
target_signature = Objects[Ships[instance].objnum].net_signature;
|
||||
}
|
||||
}
|
||||
|
||||
PACK_BYTE( umode );
|
||||
|
||||
@@ -2692,7 +2692,7 @@ void process_ship_kill_packet( ubyte *data, header *hinfo )
|
||||
|
||||
// do the normal thing when not ingame joining. When ingame joining, simply kill off the ship.
|
||||
if ( !(Net_player->flags & NETINFO_FLAG_INGAME_JOIN) ) {
|
||||
ship_hit_kill( sobjp, oobjp, nullptr, percent_killed, sd );
|
||||
ship_hit_kill( sobjp, oobjp, nullptr, percent_killed, sd != 0 );
|
||||
} else {
|
||||
sobjp->flags.set(Object::Object_Flags::Should_be_dead);
|
||||
ship_cleanup(sobjp->instance, SHIP_DESTROYED);
|
||||
|
||||
+5
-4
@@ -33286,7 +33286,7 @@ SCP_vector<sexp_help_struct> Sexp_help = {
|
||||
"\t1+:\tName of ships to deactivate shields on." },
|
||||
|
||||
{ OP_SHIP_GUARDIAN, "ship-guardian\r\n"
|
||||
"\tCauses the ships listed in this sexpression to not be killable by weapons. Use with caution!!!!\r\n\r\n"
|
||||
"\tCauses the ships listed in this sexpression to not take any damage below 1% of hull strength. As such, the ship will not be killable by weapons. Use with caution!!!!\r\n\r\n"
|
||||
"Takes 1 or more arguments...\r\n"
|
||||
"\t1+:\tName of ships to make unkillable." },
|
||||
|
||||
@@ -33297,7 +33297,7 @@ SCP_vector<sexp_help_struct> Sexp_help = {
|
||||
|
||||
// Goober5000
|
||||
{ OP_SHIP_GUARDIAN_THRESHOLD, "ship-guardian-threshold\r\n"
|
||||
"\tSame as ship-guardian, except the lowest possible hull value is specified by the sexp rather than defaulting to 1.\r\n"
|
||||
"\tSame as ship-guardian, except the lowest possible hull value (as a percentage) is specified by the sexp rather than defaulting to 1.\r\n"
|
||||
"Call with a threshold of 0 (or use ship-no-guardian) to deactivate.\r\n\r\n"
|
||||
"Takes 2 or more arguments...\r\n"
|
||||
"\t1:\tThreshold value.\r\n"
|
||||
@@ -34786,7 +34786,8 @@ SCP_vector<sexp_help_struct> Sexp_help = {
|
||||
},
|
||||
|
||||
{ OP_CUTSCENES_SHOW_SUBTITLE, "show-subtitle (deprecated)\r\n"
|
||||
"\tDisplays a subtitle, either an image or a string of text. As this operator tries to combine two functions into one and does not adjust coordinates for screen formats, it has been deprecated.\r\n"
|
||||
"\tDisplays a subtitle, either an image or a string of text. Please note that, in images without an alpha channel, black pixels will be treated as transparent. In images with an alpha channel, black pixels will be drawn as expected.\r\n\r\n"
|
||||
"As this operator tries to combine two functions into one and does not adjust coordinates for screen formats, it has been deprecated.\r\n\r\n"
|
||||
"Takes 4 to 13 arguments...\r\n"
|
||||
"\t1:\tX position (negative value to be from right of screen)\r\n"
|
||||
"\t2:\tY position (negative value to be from bottom of screen)\r\n"
|
||||
@@ -34822,7 +34823,7 @@ SCP_vector<sexp_help_struct> Sexp_help = {
|
||||
},
|
||||
|
||||
{ OP_CUTSCENES_SHOW_SUBTITLE_IMAGE, "show-subtitle-image\r\n"
|
||||
"\tDisplays a subtitle in the form of an image.\r\n"
|
||||
"\tDisplays a subtitle in the form of an image. Please note that, in images without an alpha channel, black pixels will be treated as transparent. In images with an alpha channel, black pixels will be drawn as expected.\r\n\r\n"
|
||||
"Takes 8 to 10 arguments...\r\n"
|
||||
"\t1:\tImage to display\r\n"
|
||||
"\t2:\tX position, from 0 to 100% (positive measures from the left; negative measures from the right)\r\n"
|
||||
|
||||
@@ -983,11 +983,11 @@ ADE_FUNC(getCenterPosition, l_Ship, nullptr, "Returns the position of the ship's
|
||||
return ade_set_args(L, "o", l_Vector.Set(center_pos));
|
||||
}
|
||||
|
||||
ADE_FUNC(kill, l_Ship, "[object Killer, vector Hitpos]", "Kills the ship. Set \"Killer\" to the ship you are killing to self-destruct, and \"Hitpos\" to the world coordinates of the weapon impact", "boolean", "True if successful, false or nil otherwise")
|
||||
ADE_FUNC(kill, l_Ship, "[object Killer, vector Hitpos]", "Kills the ship. Set \"Killer\" to a ship (or a weapon fired by that ship) to credit it for the kill in the mission log. Set it to the ship being killed to self-destruct. Set \"Hitpos\" to the world coordinates of the weapon impact.", "boolean", "True if successful, false or nil otherwise")
|
||||
{
|
||||
object_h *victim, *killer = nullptr;
|
||||
vec3d *hitpos = nullptr;
|
||||
if(!ade_get_args(L, "o|oo", l_Ship.GetPtr(&victim), l_Ship.GetPtr(&killer), l_Vector.GetPtr(&hitpos)))
|
||||
if(!ade_get_args(L, "o|oo", l_Ship.GetPtr(&victim), l_Object.GetPtr(&killer), l_Vector.GetPtr(&hitpos)))
|
||||
return ADE_RETURN_NIL;
|
||||
|
||||
if(!victim->IsValid())
|
||||
@@ -996,13 +996,11 @@ ADE_FUNC(kill, l_Ship, "[object Killer, vector Hitpos]", "Kills the ship. Set \"
|
||||
if(killer && !killer->IsValid())
|
||||
return ADE_RETURN_NIL;
|
||||
|
||||
//Ripped straight from shiphit.cpp
|
||||
float percent_killed = -get_hull_pct(victim->objp);
|
||||
if (percent_killed > 1.0f){
|
||||
percent_killed = 1.0f;
|
||||
}
|
||||
// use the current hull percentage for damage-after-death purposes
|
||||
// (note that this does not actually affect scoring)
|
||||
float percent_killed = get_hull_pct(victim->objp);
|
||||
|
||||
ship_hit_kill(victim->objp, killer ? killer->objp : nullptr, hitpos, percent_killed, (killer && victim->sig == killer->sig) ? 1 : 0);
|
||||
ship_hit_kill(victim->objp, killer ? killer->objp : nullptr, hitpos, percent_killed, (killer && victim->sig == killer->sig), true);
|
||||
|
||||
return ADE_RETURN_TRUE;
|
||||
}
|
||||
|
||||
+26
-22
@@ -529,6 +529,7 @@ flag_def_list_new<Weapon::Info_Flags> ai_tgt_weapon_flags[] = {
|
||||
{ "die on lost lock", Weapon::Info_Flags::Die_on_lost_lock, true, false },
|
||||
{ "no impact spew", Weapon::Info_Flags::No_impact_spew, true, false },
|
||||
{ "require exact los", Weapon::Info_Flags::Require_exact_los, true, false },
|
||||
{ "multilock target dead subsys", Weapon::Info_Flags::Multilock_target_dead_subsys, true, false },
|
||||
};
|
||||
|
||||
const int num_ai_tgt_weapon_info_flags = sizeof(ai_tgt_weapon_flags) / sizeof(flag_def_list_new<Weapon::Info_Flags>);
|
||||
@@ -5613,15 +5614,23 @@ DCF_BOOL( show_velocity_dot, ship_show_velocity_dot )
|
||||
|
||||
static bool ballistic_possible_for_this_ship(const ship_info *sip)
|
||||
{
|
||||
if (sip->allowed_bank_restricted_weapons.empty())
|
||||
// has no weapons!
|
||||
if (sip->num_primary_banks < 1)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++)
|
||||
{
|
||||
for (int j = 0; j < weapon_info_size(); ++j)
|
||||
{
|
||||
if (sip->allowed_bank_restricted_weapons[i][j] && (Weapon_info[j].wi_flags[Weapon::Info_Flags::Ballistic]))
|
||||
return true;
|
||||
// check default weapons
|
||||
if (sip->primary_bank_weapons[i] >= 0 && Weapon_info[sip->primary_bank_weapons[i]].wi_flags[Weapon::Info_Flags::Ballistic])
|
||||
return true;
|
||||
|
||||
// check allowed weapons
|
||||
if (!sip->allowed_bank_restricted_weapons.empty()) {
|
||||
for (int j = 0; j < weapon_info_size(); ++j)
|
||||
{
|
||||
if (sip->allowed_bank_restricted_weapons[i][j] && (Weapon_info[j].wi_flags[Weapon::Info_Flags::Ballistic]))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5651,15 +5660,7 @@ static void ship_parse_post_cleanup()
|
||||
}
|
||||
|
||||
// be friendly; ensure ballistic flags check out
|
||||
if (pbank_capacity_specified) {
|
||||
if ( !ballistic_possible_for_this_ship(&(*sip)) ) {
|
||||
Warning(LOCATION, "Pbank capacity specified for non-ballistic-primary-enabled ship %s.\nResetting capacities to 0.\nTo fix this, add a ballistic primary to the list of allowed primaries.\n", sip->name);
|
||||
|
||||
for (j = 0; j < MAX_SHIP_PRIMARY_BANKS; j++) {
|
||||
sip->primary_bank_ammo_capacity[j] = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!pbank_capacity_specified) {
|
||||
if ( ballistic_possible_for_this_ship(&(*sip)) ) {
|
||||
Warning(LOCATION, "Pbank capacity not specified for ballistic-primary-enabled ship %s.\nDefaulting to capacity of 1 per bank.\n", sip->name);
|
||||
|
||||
@@ -8980,7 +8981,7 @@ static void ship_auto_repair_frame(int shipnum, float frametime)
|
||||
if (objp->hull_strength > sp->ship_max_hull_strength)
|
||||
objp->hull_strength = sp->ship_max_hull_strength;
|
||||
else if (objp->hull_strength < 0)
|
||||
ship_hit_kill(objp, nullptr, nullptr, 0, 1);
|
||||
ship_hit_kill(objp, nullptr, nullptr, 0, true);
|
||||
}
|
||||
|
||||
// only allow for the auto-repair of subsystems on small ships
|
||||
@@ -17374,16 +17375,19 @@ void ship_replace_active_texture(int ship_index, const char* old_name, const cha
|
||||
}
|
||||
}
|
||||
|
||||
int texture = bm_load(new_name);
|
||||
if (final_index >= 0) {
|
||||
int texture = bm_load(new_name);
|
||||
|
||||
if (shipp->ship_replacement_textures == NULL) {
|
||||
shipp->ship_replacement_textures = (int*)vm_malloc(MAX_REPLACEMENT_TEXTURES * sizeof(int));
|
||||
if (shipp->ship_replacement_textures == nullptr) {
|
||||
shipp->ship_replacement_textures = (int*)vm_malloc(MAX_REPLACEMENT_TEXTURES * sizeof(int));
|
||||
|
||||
for (int i = 0; i < MAX_REPLACEMENT_TEXTURES; i++)
|
||||
shipp->ship_replacement_textures[i] = -1;
|
||||
}
|
||||
for (int i = 0; i < MAX_REPLACEMENT_TEXTURES; i++)
|
||||
shipp->ship_replacement_textures[i] = -1;
|
||||
}
|
||||
|
||||
shipp->ship_replacement_textures[final_index] = texture;
|
||||
shipp->ship_replacement_textures[final_index] = texture;
|
||||
} else
|
||||
Warning(LOCATION, "Invalid texture '%s' used for replacement texture", old_name);
|
||||
}
|
||||
|
||||
// function to return true if support ships are allowed in the mission for the given object.
|
||||
|
||||
+18
-4
@@ -1782,7 +1782,7 @@ static void ship_vaporize(ship *shipp)
|
||||
}
|
||||
|
||||
// *ship_objp was hit and we've determined he's been killed! By *other_obj!
|
||||
void ship_hit_kill(object *ship_objp, object *other_obj, vec3d *hitpos, float percent_killed, int self_destruct)
|
||||
void ship_hit_kill(object *ship_objp, object *other_obj, vec3d *hitpos, float percent_killed, bool self_destruct, bool always_log_other_obj)
|
||||
{
|
||||
Assert(ship_objp); // Goober5000 - but not other_obj, not only for sexp but also for self-destruct
|
||||
|
||||
@@ -1855,7 +1855,21 @@ void ship_hit_kill(object *ship_objp, object *other_obj, vec3d *hitpos, float pe
|
||||
}
|
||||
}
|
||||
killer_damage_percent = (int)(sp->damage_ship[killer_index]/sp->total_damage_received * 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// are we going to insist that other_obj was the killer?
|
||||
if (always_log_other_obj && other_obj) {
|
||||
object *named_objp = other_obj;
|
||||
|
||||
if (named_objp->type == OBJ_WEAPON && named_objp->parent != -1) {
|
||||
named_objp = &Objects[other_obj->parent];
|
||||
}
|
||||
if (named_objp->type == OBJ_SHIP) {
|
||||
killer_objp = nullptr;
|
||||
killer_ship_name = Ships[named_objp->instance].ship_name;
|
||||
killer_damage_percent = (int)(percent_killed * 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if(!self_destruct){
|
||||
// multiplayer
|
||||
@@ -1982,7 +1996,7 @@ void ship_self_destruct( object *objp )
|
||||
}
|
||||
|
||||
// self destruct
|
||||
ship_hit_kill(objp, nullptr, nullptr, 1.0f, 1);
|
||||
ship_hit_kill(objp, nullptr, nullptr, 1.0f, true);
|
||||
}
|
||||
|
||||
extern int Homing_hits, Homing_misses;
|
||||
@@ -2522,7 +2536,7 @@ static void ship_do_damage(object *ship_objp, object *other_obj, vec3d *hitpos,
|
||||
}
|
||||
|
||||
if ( !(shipp->flags[Ship::Ship_Flags::Dying]) && !MULTIPLAYER_CLIENT) { // if not killed, then kill
|
||||
ship_hit_kill(ship_objp, other_obj, hitpos, percent_killed, 0);
|
||||
ship_hit_kill(ship_objp, other_obj, hitpos, percent_killed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ void ship_apply_global_damage(object *ship_obj, object *other_obj, vec3d *force_
|
||||
void ship_apply_wash_damage(object *ship_obj, object *other_obj, float damage);
|
||||
|
||||
// next routine needed for multiplayer
|
||||
void ship_hit_kill( object *ship_obj, object *other_obj, vec3d *hitpos, float percent_killed, int self_destruct);
|
||||
void ship_hit_kill( object *ship_obj, object *other_obj, vec3d *hitpos, float percent_killed, bool self_destruct = false, bool always_log_other_obj = false);
|
||||
|
||||
void ship_self_destruct( object *objp );
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ typedef struct weapon {
|
||||
|
||||
int target_num; // Object index of target
|
||||
int target_sig; // So we know if the target is the same one we've been tracking
|
||||
float nearest_dist; // nearest distance yet attained to target
|
||||
fix creation_time; // time at which created, stuffed Missiontime
|
||||
flagset<Weapon::Weapon_Flags> weapon_flags; // bit flags defining behavior, see WF_xxxx
|
||||
object* homing_object; // object this weapon is homing on.
|
||||
|
||||
@@ -88,6 +88,7 @@ namespace Weapon {
|
||||
Can_damage_shooter, // this weapon and any of its descendants can damage its shooter - Asteroth
|
||||
Heals, // 'damage' heals instead of actually damaging - Asteroth
|
||||
No_collide,
|
||||
Multilock_target_dead_subsys,
|
||||
|
||||
NUM_VALUES
|
||||
};
|
||||
|
||||
+6
-49
@@ -187,6 +187,7 @@ flag_def_list_new<Weapon::Info_Flags> Weapon_Info_Flags[] = {
|
||||
{ "can damage shooter", Weapon::Info_Flags::Can_damage_shooter, true, false },
|
||||
{ "heals", Weapon::Info_Flags::Heals, true, false },
|
||||
{ "no collide", Weapon::Info_Flags::No_collide, true, false },
|
||||
{ "multilock target dead subsys", Weapon::Info_Flags::Multilock_target_dead_subsys, true, false },
|
||||
};
|
||||
|
||||
const size_t num_weapon_info_flags = sizeof(Weapon_Info_Flags) / sizeof(flag_def_list_new<Weapon::Info_Flags>);
|
||||
@@ -5429,54 +5430,7 @@ void weapon_process_post(object * obj, float frame_time)
|
||||
}
|
||||
#else
|
||||
weapon_maybe_play_flyby_sound(obj, wp);
|
||||
#endif
|
||||
|
||||
// If our target is still valid, then update some info.
|
||||
if (wp->target_num != -1) {
|
||||
if (Objects[wp->target_num].signature == wp->target_sig) {
|
||||
float cur_dist;
|
||||
vec3d v0;
|
||||
|
||||
vm_vec_avg(&v0, &obj->pos, &obj->last_pos);
|
||||
|
||||
cur_dist = vm_vec_dist_quick(&v0, &Objects[wp->target_num].pos);
|
||||
|
||||
if (cur_dist < wp->nearest_dist) {
|
||||
wp->nearest_dist = cur_dist;
|
||||
} else if (cur_dist > wp->nearest_dist + 1.0f) {
|
||||
float dot;
|
||||
vec3d tvec;
|
||||
ai_info *parent_aip;
|
||||
|
||||
parent_aip = NULL;
|
||||
if (obj->parent != Player_obj-Objects) {
|
||||
parent_aip = &Ai_info[Ships[Objects[obj->parent].instance].ai_index];
|
||||
}
|
||||
|
||||
vm_vec_normalized_dir(&tvec, &v0, &Objects[wp->target_num].pos);
|
||||
dot = vm_vec_dot(&tvec, &Objects[wp->target_num].orient.vec.fvec);
|
||||
wp->target_num = -1;
|
||||
|
||||
// Learn! If over-shooting or under-shooting, compensate.
|
||||
// Really need to compensate for left/right errors. This does no good against someone circling
|
||||
// in a plane perpendicular to the attacker's forward vector.
|
||||
if (parent_aip != NULL) {
|
||||
if (cur_dist > 100.0f)
|
||||
parent_aip->lead_scale = 0.0f;
|
||||
|
||||
if (dot < -0.1f){
|
||||
parent_aip->lead_scale += cur_dist/2000.0f;
|
||||
} else if (dot > 0.1f) {
|
||||
parent_aip->lead_scale -= cur_dist/2000.0f;
|
||||
}
|
||||
|
||||
if (fl_abs(parent_aip->lead_scale) > 1.0f){
|
||||
parent_aip->lead_scale *= 0.9f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if(wip->wi_flags[Weapon::Info_Flags::Particle_spew] && wp->lssm_stage != 3 ){
|
||||
weapon_maybe_spew_particle(obj);
|
||||
@@ -5741,7 +5695,6 @@ void weapon_set_tracking_info(int weapon_objnum, int parent_objnum, int target_o
|
||||
if ((target_objnum > -1) && (target_objnum < (MAX_OBJECTS)) && can_lock) {
|
||||
wp->target_num = target_objnum;
|
||||
wp->target_sig = Objects[target_objnum].signature;
|
||||
wp->nearest_dist = 99999.0f;
|
||||
if ( (wip->wi_flags[Weapon::Info_Flags::Homing_aspect]) && target_is_locked) {
|
||||
wp->homing_object = &Objects[target_objnum];
|
||||
wp->homing_subsys = target_subsys;
|
||||
@@ -9182,6 +9135,10 @@ bool weapon_multilock_can_lock_on_subsys(object* shooter, object* target, ship_s
|
||||
if (target_subsys->flags[Ship::Subsystem_Flags::Untargetable])
|
||||
return false;
|
||||
|
||||
//by not checking for max_hits > 0 here, subsys' with disabled hitpoints are also excluded.
|
||||
if (!wip->wi_flags[Weapon::Info_Flags::Multilock_target_dead_subsys] && target_subsys->current_hits <= 0.0f)
|
||||
return false;
|
||||
|
||||
vec3d ss_pos;
|
||||
get_subsystem_world_pos(target, target_subsys, &ss_pos);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
set(FSO_VERSION_MAJOR 21)
|
||||
set(FSO_VERSION_MINOR 4)
|
||||
set(FSO_VERSION_BUILD 0)
|
||||
set(FSO_VERSION_REVISION 0)
|
||||
set(FSO_VERSION_REVISION_STR )
|
||||
Reference in new issue
Block a user